【问题标题】:Cancelling a BackgroundWorker, how to prevent race when it has already finished取消BackgroundWorker,如何在已经完成时防止比赛
【发布时间】:2010-01-08 20:17:51
【问题描述】:

我正在使用 BackgroundWorker 来执行长时间的计算(一次只有一个这样的计算)。

用户有可能取消worker(调用worker.CancelAsync)。

在 worker.DoWork 方法中,我会定期检查取消挂起标志,然后从该方法返回。

然后从工作人员引发 Completed 事件,我可以检查工作人员是否被取消。此外,重要的是,当检测到取消时,我会进行一些额外的清理。

我确信如果用户取消了 worker 并且它已经从 DoWork 方法返回,那么可能会出现问题。在那种情况下,我真的很想知道工人被取消了,所以我可以清理......

有没有更好的方法来处理一个工人的取消过程?

【问题讨论】:

    标签: .net multithreading backgroundworker


    【解决方案1】:

    您的DoWork 事件处理程序应定期检查BackgroundWorker.CancellationPending,并在取消之前将DoWorkEventArgs.Cancel 设置为true。

    您的RunWorkerCompleted 事件处理程序应检查RunWorkerCompletedEventArgs.Cancelled 属性以确定DoWork 事件处理程序是否已取消(将DoWorkEventArgs.Cancel 设置为true)。

    如果出现竞争条件,可能会发生用户请求取消(BackgroundWorker.CancellationPending 为真)但工作人员没有看到它(RunWorkerCompletedEventArgs.Cancelled 为假)。您可以测试这两个属性以确定是否发生了这种情况,并执行您选择的任何操作(将其视为成功完成 - 因为工作人员确实成功完成了,或者视为取消 - 因为用户已取消并且不关心任何更多)。

    我没有看到任何对所发生的事情有任何歧义的情况。

    编辑

    作为对评论的回应 - 如果有多个类需要检测 CancellationPending,那么除了向这些类传递对诸如 BackgroundWorker 之类的类型的引用以允许它们检索此信息之外,别无选择。您可以将其抽象为一个接口,这就是我在对有关 BackgroundWorkers 的问题的回复中所描述的通常所做的事情。但是您仍然需要将实现此接口的类型的引用传递给您的工作程序类。

    如果您希望您的工作类能够设置DoWorkEventArgs.Cancel,您将需要传递对此的引用,或者采用不同的约定(例如布尔返回值或自定义异常),以允许您的工作类表示已取消。

    【讨论】:

    • 这是一个非常清楚的解释。但是,由于工作不仅仅在“DoWork”方法中完成,我必须将 EventArgs 传递给每个检查取消的类,并且我有大约 30 个这些类(深度优化问题)。这是可行的,但我觉得那将是一个 PITA ......我也不喜欢那些不得不管理取消的课程。我仍在寻找更好的方法。
    • “我也不喜欢那些不得不取消的课程”——我不明白你的担忧。如果是这样,不要让他们管理取消!相反,仅当这些类之一将控制权返回给您的主 DoWork 方法时才检查取消。
    • 那些内部类的计算时间很长,因此它们不会经常返回。
    • 我认为这个答案是不正确的。制作一个创建 BackgroundWorker 并立即取消它的程序。让工作人员睡半秒钟以确保它已被取消(如果您愿意,可以使用 MessageBox 检查),但不要设置 e.Cancel。然后检查你完成的函数,你会看到 e.Cancelled 和你线程的 CancellationPending 都是假的。所以你不知道取消请求。
    【解决方案2】:

    我认为这里的其他答案实际上并不能解决所有情况下可能出现的竞争条件。问题是 CancellationPending 似乎在 DoWork 结束后被设置回 false ,无论 DoWork 是否真的有机会检查标志。如果你愿意,你可以自己测试一下。为了解决这个问题,我必须创建一个子类,如下所示:

    Public Class CancelTrackingBackgroundWorker
      Inherits System.ComponentModel.BackgroundWorker
      Public CancelRequested As Boolean = False
      Public Sub TrackableCancelAsync()
        Me.CancelRequested = True
        Me.CancelAsync()
      End Sub
    End Class
    

    (顺便说一句,令人讨厌的 CancelAsync 是多么令人讨厌。)然后我调用 TrackableCancelAsync 而不是旧的 CancelAsync 并在我的代码中检查这个新的 CancelRequested 标志。只要您只从 UI 线程调用和检查(应该是标准的),那应该可以解决您的线程问题。

    【讨论】:

      【解决方案3】:

      保证您不会在 RunWorkerCompleted 事件处理程序中发生争用,因为它在 UI 线程上运行。这应该始终有效:

      private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
        var bgw = sender as BackgroundWorker;
        if (e.Cancelled || bgw.CancellationPending) {
          // etc..
        }
      }
      

      【讨论】:

        【解决方案4】:

        有什么原因你不能在异步运行的方法结束时进行清理?

        我一般使用这样的结构

        private void MyThreadMethod()
        {
            try
            {
                // Thread code
            }
            finally 
            {
                // Cleanup
            }
        }
        

        我只使用完成事件来完成更新 UI 之类的任务,而不是在线程之后进行清理。

        【讨论】:

        • 是的,我无法在后台工作人员中进行清理...我需要在 UI 线程上进行。
        • 您是否多次看到事件触发?如果是这样,您可以在事件处理程序中使用 lock(...) {} 块来确保代码一次只能执行一次,并设置一个指示清理完成的标志(首先检查该标志在你的锁定块,如果未设置,则仅运行清理代码)。
        【解决方案5】:

        如果没有看到您的代码,很难提出建议,但您可以做的一件事是在用户单击“取消”按钮时禁用它,并在退出 DoWork 方法时禁用它。

        如果用户也可以通过按键取消,那么您也需要禁用它。

        另外——如果DoWork 方法已经完成,那么用户就没有什么可以“取消”的了——或者我错过了什么。在这种情况下,即使您没有禁用取消按钮,您也无需执行任何额外操作。

        为什么工作人员完成工作后需要查看取消标志?是否仍要回滚已更改的内容?

        【讨论】:

        • 事实上,问题不在于阻止用户多次取消......问题更多的是确保即使在工作人员完成工作并收到 Completed 事件之后我也能看到取消标志.
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多