【问题标题】:Correct usage of TPL to launch a cancellable ASync operation正确使用 TPL 启动可取消的异步操作
【发布时间】:2015-07-29 14:04:23
【问题描述】:

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

 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 窗体应用程序。

【问题讨论】:

    标签: .net vb.net winforms task-parallel-library


    【解决方案1】:

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

    处理此问题的正确方法是在访问之前查看dt.Exception,然后再尝试访问dt.Result。当您访问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,只会使问题更加复杂。

    【讨论】:

    • 谢谢佩德罗。通常我访问的是聚合异常,这就解释了为什么我们只在这个屏幕上看到这个。我认为访问异常可以解决这里的问题,但我担心我所采用的方法可能存在根本性的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-15
    • 1970-01-01
    • 2016-09-19
    • 1970-01-01
    相关资源
    最近更新 更多