正确使用 TPL 来启动可取消的异步操作

Correct usage of TPL to launch a cancellable ASync operation

我一直在使用 TPL 在非 UI 线程中 运行 数据库提取,允许 UI 在它们发生时继续进行。下面示例中的代码被调用以填充主细节视图中的细节窗格。主窗格中有一个树视图,根据单击的节点获取不同的数据。 UI 允许用户取消抓取,如果他们 select 在抓取处于活动状态时使用不同的节点,则自动取消抓取。这是我用来执行此操作的代码:

 Protected Overrides Sub FetchSummary()
  If DBKey.PresentAndSet(DataKey) Then
    _view.BeginDataFetch()
    ' Cancel any active refresh
    If TaskCancelTokenSource IsNot Nothing Then TaskCancelTokenSource.Cancel()
    TaskCancelTokenSource = New CancellationTokenSource
    Dim ctok = TaskCancelTokenSource.Token
    Dim dataTask = New Task(Of IEnumerable(Of IAssignSailingPart.ISummary))(Function() FetchsummaryData(Context, DataKey), ctok)
    Dim uiSyncContext = TaskScheduler.FromCurrentSynchronizationContext
    dataTask.ContinueWith(Sub(dt) _view.Data = dt.Result, ctok, TaskContinuationOptions.OnlyOnRanToCompletion, uiSyncContext)
    dataTask.ContinueWith(Sub(dt) _view.FailDataFetch("There was an error fetching the data, try refreshing"), Nothing, TaskContinuationOptions.OnlyOnFaulted, uiSyncContext)
    dataTask.ContinueWith(Sub(dt) _view.Data = New List(Of IAssignSailingPart.ISummary), Nothing, TaskContinuationOptions.OnlyOnCanceled, uiSyncContext)
    dataTask.Start()
  End If
End Sub

因此,为了开始任务,我们调用一个函数来查询数据库以获取我们的结果。成功时我们将其发送到视图,取消时我们将空数据集发送到视图,失败时我们告诉用户尝试刷新。

这一切似乎 工作正常。用户对响应速度等感到满意。最近我们遇到了一些问题,尽管数据库服务器遇到了一些不相关的问题。当获取应用程序的编译版本失败时(与 IDE 中相反),它会在实际失败发生后不久以未捕获的聚合异常终止应用程序。我已经对此进行了一些研究,并且了解(或者我认为)在任务被垃圾收集时异常是在不同的线程上抛出的。

我的问题是我应该如何调整代码来正确处理这个问题?这是用于使用 .Net 4.0 的 windows 表单应用程序。

您遇到的问题与未观察到故障任务中的异常有关(在本例中为 dt)。每个 Task 对象都带有一个标志,指示其异常(如果有)是否为 observed/accessed。当 Task 对象最终完成并且该标志指示未处理异常时,应用程序将被 .NET 4.0 关闭。顺便说一句,这是在 .NET 4.5 中已更改的行为。 Stephen Toub 详细解释 here.

处理此问题的正确方法是在访问 dt.Result 之前查看 dt.Exception。当您访问 dt.Exception 属性(为了决定下一步做什么,或者只是记录异常)时,这将导致 Task 异常被标记为已观察到,并且Task 实例完成后,应用程序将不再崩溃。另一方面,如果 Task 出错,则直接访问 dt.Result 只会传播(重新抛出)异常。

我也会做一个 ContinueWith() 调用,并在那里检查 Task 状态(请原谅我的 VB,我是 C# 开发人员):

 Protected Overrides Sub FetchSummary()
  If DBKey.PresentAndSet(DataKey) Then
    _view.BeginDataFetch()
    ' Cancel any active refresh
    If TaskCancelTokenSource IsNot Nothing Then TaskCancelTokenSource.Cancel()
    TaskCancelTokenSource = New CancellationTokenSource
    Dim ctok = TaskCancelTokenSource.Token
    Dim dataTask = New Task(Of IEnumerable(Of IAssignSailingPart.ISummary))(Function() FetchsummaryData(Context, DataKey), ctok)
    Dim uiSyncContext = TaskScheduler.FromCurrentSynchronizationContext
    dataTask.ContinueWith(Sub(dt) 
       If dt.IsFaulted Then
           _view.FailDataFetch("There was an error fetching the data, try refreshing")
           Exit Sub
       End If
       If dt.IsCancelled Then
           _view.Data = New List(Of IAssignSailingPart.ISummary)
           Exit Sub
       End If

       _view.Data = dt.Result
    End Sub, ctok, uiSyncContext)
    dataTask.Start()
  End If
End Sub

原因是,当您用 TaskContinuationOptions.OnlyOnRanToCompletion 标记 Task 时,如果 dataTask 没有,则继续 Task 将被标记为 Cancelled 运行 完成,只会使问题复杂化。