65.9K
CodeProject 正在变化。 阅读更多。
Home

AsyncWorker - 类型安全的 BackgroundWorker(以及关于多线程的一般性讨论)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (21投票s)

2010年2月21日

CPOL

9分钟阅读

viewsIcon

59903

downloadIcon

1868

泛型方法实现类型安全的线程。

引言

线程通常用于处理耗时过长的进程,就好像你可以等待它一样。尤其是运行 GUI 的线程,必须避免执行耗时的操作,因为用户输入可能随时发生。如果 GUI 线程因某个特定操作忙碌超过大约 100 毫秒,用户会立即感到“卡顿”或“应用程序无响应”。

现在已经(不幸地)过时了

2012 年,Microsoft 推出了 Async/Await 模式,作为当前线程处理的“最新技术”。
与此处发布的方法相比,Async/Await 能够更好地、更令人信服地解决 5 个线程问题(尽管此方法也是一个非常好的方法!)。
因此,请参考我的 Async/Await 文章

示例应用程序

输入是鼠标点击彩色表面,以及点击发生的时间。输出会将 `Label` 放置在鼠标点击位置,并显示发生时间。通过调用 `Thread.Sleep()` 来模拟耗时的 D 数据评估。

AsyncWorker2/PoorGui.gif

我实现了此行为的多种变体,以演示不同复杂程度的级别。

过程式设计

线程意味着对正常顺序的基本重组。正常顺序可以先评估数据,然后显示它。操作的顺序直接写入代码中。

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   //evaluate data
   System.Threading.Thread.Sleep(1000);
   var p = e.Location;
   var s=string.Format("Position {0} / {1}\nclicked at {2:T}", 
      p.X, p.Y, DateTime.Now);
   //display result
   label1.Text = s;
   label1.Location = p - label1.Size;
}

这是最低的复杂程度,并且更可取,但它会阻塞 GUI 线程。

线程永不返回 - “转发设计”

线程处理的根本原则是,它总是无返回值地工作。一个并行运行的函数永远无法返回到它被调用的地方,因为那个地方已经运行过了(甚至是在*并行*中)。“等待辅助线程”的想法是相当矛盾的,因为启动辅助线程的目的就是为了摆脱等待的需要。
并行运行的方法只能将其结果转发给另一个方法,而不能返回

转发设计

为了摆脱上述代码的阻塞行为,必须将昂贵的部分隔离出来,以便将进程分为三个方法:`BeforeBlocking`、`Blocking`、`AfterBlocking`。这些方法必须是 `void`,并且一个调用另一个,因为在下一步开发中,它们将在线程之间切换,因此将没有 `return` 选项。

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   EvaluateData(DateTime.Now, e.Location);
}

private void EvaluateData(DateTime t, Point p) {
   System.Threading.Thread.Sleep(1000);
   var s = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t);
   DisplayResult(s, p);
}

private void DisplayResult(string s, Point p) {
   label1.Text = s;
   label1.Location = p - label1.Size;
}

也可以通过事件更间接地进行转发(我稍后会展示)。实际上,***线程意味着从基于过程的编程转变为基于事件的编程***。一旦中间部分 `Blocking`(或 `EvaluateData`)被转移到辅助线程,转发设计就会出现为基于事件的,因为 `AfterBlocking`(或 `DisplayResult`)是一个典型的*回调方法*。
`AfterBlocking`*必须*重新传输到 GUI 线程,因为任何 `Control` 如果被非创建它的线程访问,都会抛出 `InvalidOperationException`。

附注:通常,转发设计是不被推荐的,因为它容易变得混乱;)。在非线程场景中,你会依次调用 `BeforeBlocking`、`Blocking`、`AfterBlocking`,例如:

//...
BeforeBlocking();
Blocking();
AfterBlocking();
//... 

但在线程场景中,这是不可行的,因为 `Blocking()`(或者我应该称它为:`NoLongerBlocking()`?)并没有真正完成工作,只是触发了它的并行执行。(而 `AfterBlocking()` 会在 `Blocking` 工作完成之前被调用,并导致错误,因为它应该在 `Blocking()`*之后*。)

AsyncWorkerX

通过非常简单的重构,转发设计就可以被解除阻塞。

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   //EvaluateData(DateTime.Now, e.Location);
   AsyncWorkerX.RunAsync(EvaluateData, DateTime.Now, e.Location);
}

private void EvaluateData(DateTime t, Point p) {
   System.Threading.Thread.Sleep(1000);
   var s = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t);
   //DisplayResult(s, p);
   AsyncWorkerX.NotifyGui(DisplayResult, s, p);
}

private void DisplayResult(string s, Point p) {
   label1.Text = s;
   label1.Location = p - label1.Size;
}

`AsyncWorkerX` 是一个 `static` 类,发布了通用的(扩展)方法 `RunAsync()` 和 `NotifyGui()`,每个方法都有五个重载。它们接受最多四个参数的方法委托,以及所需的参数。
委托和参数的类型由类型推断推断出来,这使得使用非常灵活和简单,而不会丢失类型安全性。
这是引入的线程设计的主要优点。

BackgroundWorker

`BackgroundWorker` 完全无法重现上面所示的行为。注意:每次鼠标点击都会启动一个新线程,如果你快速点击三次,就会有三个线程运行,它们会相互重叠。
`BackgroundWorker` 会抛出它的“`IsBusy`”- `(InvalidOperation-) Exception`。

struct Data2Thread {
   public DateTime Time;
   public Point Pt;
}
struct Data2Gui {
   public string Text;
   public Point Pt;
}

private void ucl_MouseDown(object sender, MouseEventArgs e) {
   if(backgroundWorker1.IsBusy) {
      MessageBox.Show("backgroundWorker1.IsBusy");
      return;
   }
   Data2Thread d = new Data2Thread() { Time = DateTime.Now, Pt = e.Location };
   backgroundWorker1.RunWorkerAsync(d);
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
   Data2Thread d = (Data2Thread)e.Argument;           
   System.Threading.Thread.Sleep(1000);
   Data2Gui d2g = new Data2Gui() {
      Pt = d.Pt,
      Text = string.Format(
         "Position {0} / {1}\nclicked at {2:T}", d.Pt.X, d.Pt.Y, d.Time)
   };
   e.Result = d2g;
}

private void backgroundWorker1_RunWorkerCompleted(
      object sender, RunWorkerCompletedEventArgs e) {
   Data2Gui d2g = (Data2Gui)e.Result;
   label1.Text = d2g.Text;
   label1.Location = d2g.Pt - label1.Size;
}

复杂性方面出现了一个巨大的进步,因为 `BackgroundWorker` 无法在线程之间传输多个参数。您必须创建特殊的数据结构,将参数和结果包装在其中,并且必须在另一个线程中使用它们时进行强制转换和解包。

这是一种非常不直观的方法,而且原始的转发设计完全无法识别,因为 `BackgroundWorker` 的事件决定了方法名称和参数。

更丰富的 GUI

实际上,我想保持 `static` 方法不变,但 `BackgroundWorker` 支持两个我想要超越的漂亮功能:取消和报告进度。
因此,我将示例应用程序更改为创建一个更丰富的 GUI:有一个取消按钮和一个进度条,两者只有在并行进程运行时才可见。
此外,`MouseDown` 事件暂时被取消订阅,以抑制一次请求多个并行进程的选项。

AsyncWorker2/RichGui.gif

线程原则“UpdateGui”

立即,需要一个方法“`UpdateGui`”来集中处理那些根据并行进程是否正在运行来更改 GUI 外观的命令。否则,您可能会在 `MouseDown()` 中禁用一个按钮,在显示结果时重新启用它,并在取消时忘记它。

BackgroundWorker 与 AsyncWorker 对比

public partial class uclBgwPrgbar : UserControl {

   struct Data2Thread {
      public DateTime Time;
      public Point Pt;
   }
   struct Data2Gui {
      public string Text;
      public Point Pt;
   }

   public uclBgwPrgbar() {
      InitializeComponent(); 
      UpdateGui();
   }

   private void UpdateGui() {
      btCancel.Visible = progressBar1.Visible = backgroundWorker1.IsBusy;
      if(backgroundWorker1.IsBusy) {
         this.MouseDown -= ucl_MouseDown;
         progressBar1.Value = 0;
      } else { this.MouseDown += ucl_MouseDown; }
   }

   void ucl_MouseDown(object sender, MouseEventArgs e) {
      //pack arguments
      Data2Thread d = new Data2Thread() { Time = DateTime.Now, Pt = e.Location };
      backgroundWorker1.RunWorkerAsync(d);
      UpdateGui();
   }

   private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
      //unpack arguments
      Data2Thread d2t = (Data2Thread)e.Argument;           
      var p = d2t.Pt;
      var t = d2t.Time;
      //evaluate data
      for(var i = 0; i < 300; i++) {
         System.Threading.Thread.Sleep(10);
         backgroundWorker1.ReportProgress(i / 3);  //reports 300 "progresses"
         if(backgroundWorker1.CancellationPending) {
            e.Cancel = true;
            return;
         }
      }
      //pack results
      Data2Gui d2g = new Data2Gui() {
         Pt = p,
         Text = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t)
      };
      e.Result = d2g;
   }

   private void backgroundWorker1_RunWorkerCompleted(
         object sender, RunWorkerCompletedEventArgs e) {
      UpdateGui();
      if(e.Cancelled) {
         label1.Text = "Cancelled"; 
         return;
      } 
      Data2Gui d2g = (Data2Gui)e.Result;  //unpack results
      label1.Text = d2g.Text;
      label1.Location = d2g.Pt - label1.Size;
   }

   private void btCancel_Click(object sender, EventArgs e) {
      backgroundWorker1.CancelAsync();
   }

   private void backgroundWorker1_ProgressChanged(
         object sender, ProgressChangedEventArgs e) {
      progressBar1.Value = e.ProgressPercentage;
   }
}

使用 `AsyncWorker` 类完成相同的事情。

public partial class uclAsyncWorker : UserControl {
   
   private AsyncWorker _Worker = new AsyncWorker();
   
   public uclAsyncWorker() {
      InitializeComponent();
      _Worker.IsRunningChanged += (s, e) => UpdateGui();
      UpdateGui();
   }
   
   private void UpdateGui() {
      btCancel.Visible = progressBar1.Visible = _Worker.IsRunning;
      if(_Worker.IsRunning) {
         this.MouseDown -= ucl_MouseDown;
         progressBar1.Value = 0;
      } else { this.MouseDown += ucl_MouseDown; }
   }
   
   void ucl_MouseDown(object sender, MouseEventArgs e) {
      _Worker.RunAsync(EvaluateData, DateTime.Now, e.Location);
   }
   
   private void EvaluateData(DateTime t, Point p) {
      for(var i = 0; i < 300; i++) {
         System.Threading.Thread.Sleep(10);
         //reports a progress only if the previous one was more than 0.3s ago
         //ReportProgress()==false indicates the process is cancelled.
         if(!_Worker.ReportProgress(() => progressBar1.Value = i / 3))return;
      }
      var s = string.Format("Position {0} / {1}\nclicked at {2:T}", p.X, p.Y, t);
      _Worker.NotifyGui(DisplayResult, s, p);
   }
   
   private void DisplayResult(string s, Point p) {
      label1.Text = s;
      label1.Location = p - label1.Size;
   }
   
   private void btCancel_Click(object sender, EventArgs e) {
      _Worker.Cancel();
      label1.Text = "Cancelled";
   }
}

注意更简单的设计:只有一个事件 `IsRunningChanged()`,用于提供机会更改 GUI 元素,取决于 `AsyncWorker` 是否在运行。

`ReportProgress()` 和 `NotifyGui()` 的工作方式与之前提到的 `static AsyncWorkerX` 方法非常相似。这意味着:`ReportProgress()` 与 `NotifyGui()` 一样灵活——你可以执行任何你想要的操作作为进度报告——更新 `Label`,更改 `Color`,将数据记录到 `Textbox`……
`ReportProgress()` 和 `NotifyGui()` 之间的区别在于 `AsyncWorker` 的附加属性 `int ReportInterval`(表示毫秒)。
如果在该间隔内多次调用 `ReportProgress()`,则会省略报告。
此功能在快速循环中是必需的——否则,您可能会通过过于频繁地报告来破坏性能(例如,`BackgroundWorker` 的示例代码每秒报告 100 次“进度”!)。

同时注意 `bool` 的返回值(两者):返回 `false` 表示已请求取消。如果是这样,您应该立即取消并行处理并返回。`AsyncWorker` 只会告诉一次线程应该退出——下次您尝试报告进度或通知 GUI 时,`AsyncWorker` 将抛出异常。

注意取消概念的转移

`BackgroundWorker` *总是*会到达 `RunWorkerCompleted()`,即应该显示结果的方法。因此,必须处理三件非常不同的事情:

  • 重新启用在进程运行时被禁用的 GUI 元素
  • 必要时通知取消发生
  • 否则,显示结果

`AsyncWorker` 也会引发其事件 `IsRunningChanged()`,但在取消的情况下,您被强制*真正*取消,以便 `DisplayResult` 部分不再被触及。

因此,在 `DisplayResult()` 中,您负责显示结果,在 `IsRunningChanged()` 中负责调整 GUI 元素,并在请求取消时通知取消,我建议在请求取消的地方实现:即在 `btCancel_Click()` 中(或者,您可以使用 `IsRunningChanged()`,并检查 `bool` 属性 `AsyncWorker.CancelRequested`)。

数据绑定到 AsyncWorker.IsRunning

将 `AsyncWorker.IsRunning` 与公共 `event EventHandler IsRunningChanged()` 结合使用,可以实现数据绑定的选项。对于简单的 GUI,它可以帮助降低复杂性。

public partial class uclDataBoundWorker : UserControl {

   private AsyncWorker _Worker = new AsyncWorker();

   public uclDataBoundWorker() {
      InitializeComponent();
      progressBar1.DataBindings.Add("Visible", _Worker, "IsRunning");
      btCancel.DataBindings.Add("Visible", _Worker, "IsRunning");
   }

   protected override void OnMouseDown(MouseEventArgs e) {
      if(!_Worker.IsRunning) _Worker.RunAsync(EvaluateData, DateTime.Now, e.Location);
      base.OnMouseDown(e);
   }
   
   //...

您看:`UpdateGui` 方法可以被移除。

危险的 BackgroundWorker 行为

尽管 MSDN 本身 [ ^ ] 要求在使用 `aDelegate.BeginInvoke()` 启动辅助线程时无条件调用 `aDelegate.EndInvoke()`,但当您反编译 `BackgroundWorker` 类时,会发现以下代码:

public void RunWorkerAsync(object argument)
{
    if (this.isRunning)
    {
        throw new InvalidOperationException(SR.GetString
		("BackgroundWorker_WorkerAlreadyRunning"));
    }
    this.isRunning = true;
    this.cancellationPending = false;
    this.asyncOperation = AsyncOperationManager.CreateOperation(null);
    this.threadStart.BeginInvoke(argument, null, null);
}

而“`this.threadStart`”只是一个 `WorkerThreadStartDelegate` 类型的 `private` 类变量,它被定义为:

private delegate void WorkerThreadStartDelegate(object argument);

因为:

    this.threadStart.BeginInvoke(argument, null, null);

既不保存返回的 `IAsyncResult`,也不将 `AsyncCallback` 传递给 `BeginInvoke()` 调用,所以我看不到 `BackgroundWorker` 正确调用 `this.threadStart.EndInvoke(IAsyncResult ar)` 的选项。

几天来我一直很困惑,我想:“*也许调用 `aDelegate.EndInvoke()` 不是必需的,如果目标方法是 void,并且不返回任何值供 `.EndInvoke()` 评估*”。但我随后认识到一个严重的问题:**如果没有附加调试器,当异常发生在辅助线程中时,异常将丢失。**

通常,在开发软件时,*总会*附加一个调试器,它会正确捕获并断言这些辅助线程异常。因此,这意味着,在部署软件之前,您将永远不会遇到异常丢失的问题,并且在客户使用时会发生错误。由于异常丢失,软件将继续在不确定的状态下运行,并且随后的故障可能会造成重大损害,例如损坏敏感数据。错误日志也只会记录后续异常,而不会记录真正的原因。

以下代码允许尝试各种抛出的异常:

Imports System.Windows.Forms
Imports System.ComponentModel

Partial Public Class uclExceptions : Inherits UserControl

   Private _ThrowEx As Action(Of String) = AddressOf ThrowEx

   Private Sub ThrowEx(ByVal text As String)
      Throw New Exception(text)
   End Sub

   Private Sub Button_Click(ByVal sender As Object, ByVal e As EventArgs) _
	Handles btBackgroundworker.Click, _
         btAsyncworker.Click, btWithEndInvoke.Click, _
		btWithoutEndInvoke.Click, btSynchron.Click
      Select Case True
         Case sender Is btSynchron
            ThrowEx("thrown in main-thread")
         Case sender Is btBackgroundworker
            BackgroundWorker1.RunWorkerAsync()
         Case sender Is btAsyncworker
            _ThrowEx.RunAsync("thrown by Asyncworker")
         Case sender Is btWithEndInvoke
            _ThrowEx.BeginInvoke("thrown with EndInvoke", _
		AddressOf _ThrowEx.EndInvoke, Nothing)
         Case sender Is btWithoutEndInvoke
            _ThrowEx.BeginInvoke("thrown without EndInvoke", Nothing, Nothing)
      End Select
   End Sub

   Private Sub backgroundWorker1_DoWork(ByVal sender As Object, _
		ByVal e As DoWorkEventArgs)
      ThrowEx("thrown by  backgroundWorker1")
   End Sub

End Class
using System;
using System.Windows.Forms;
using System.ComponentModel;

namespace AsyncWorkerCs {
   public partial class uclExceptions : UserControl {

      private Action<string> _ThrowEx = delegate(string text) 
	{ throw new Exception(text); };

      public uclExceptions() { InitializeComponent(); }

      private void btBackgroundworker_Click(object sender, EventArgs e) {
         backgroundWorker1.RunWorkerAsync();
      }
      private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
         _ThrowEx("thrown by  backgroundWorker1");
      }
      private void btWithoutEndInvoke_Click(object sender, EventArgs e) {
         _ThrowEx.BeginInvoke("thrown without EndInvoke", null, null);
      }
      private void btWithEndInvoke_Click(object sender, EventArgs e) {
         _ThrowEx.BeginInvoke("thrown with EndInvoke", _ThrowEx.EndInvoke, null);
      }
      private void btAsyncworker_Click(object sender, EventArgs e) {
         _ThrowEx.RunAsync("thrown by Asyncworker");
      }
      private void btSynchron_Click(object sender, EventArgs e) {
         _ThrowEx("thrown in main-thread");
      }
   }
}

记录辅助线程异常

每个示例应用程序都包含一个占位符,可以在其中实现全局错误日志记录,作为最后的安全网,以获取来自辅助线程的异常信息,并防止应用程序在不确定的状态下继续运行。

Imports Microsoft.VisualBasic.ApplicationServices
Imports WinFormApp = System.Windows.Forms.Application

Namespace My

   Partial Friend Class MyApplication

      Private Sub App_Startup(ByVal sender As Object, ByVal e As StartupEventArgs) _
	Handles Me.Startup
         AddHandler AppDomain.CurrentDomain.UnhandledException, _
		AddressOf SideThread_Exception
         AddHandler WinFormApp.ThreadException, AddressOf MainThread_Exception
      End Sub

      Private Shared Sub MainThread_Exception(ByVal sender As Object, _
		ByVal e As System.Threading.ThreadExceptionEventArgs)
         MessageBox.Show("Log unhandled main-thread-Exception here" & _
		vbLf & e.Exception.ToString())
         WinFormApp.Exit()
      End Sub

      Private Shared Sub SideThread_Exception(ByVal sender As Object, _
		ByVal e As System.UnhandledExceptionEventArgs)
         MessageBox.Show("Log unhandled side-thread-Exception here" & _
		vbLf + e.ExceptionObject.ToString())
         WinFormApp.Exit()
      End Sub

   End Class

End Namespace
using System;
using System.Windows.Forms;

namespace AsyncWorkerCs {

   public static class Program {

      [STAThread]
      static void Main() {
         Application.EnableVisualStyles();
         AppDomain.CurrentDomain.UnhandledException += SideThread_Exception;
         Application.ThreadException += MainThread_Exception;
         Application.SetCompatibleTextRenderingDefault(false);
         Application.Run(new frmMain());
      }

      static void MainThread_Exception(object sender, 
		System.Threading.ThreadExceptionEventArgs e) {
         MessageBox.Show("Log unhandled main-thread-Exception here\n" + 
		e.Exception.ToString());
         Application.Exit();
      }

      static void SideThread_Exception(object sender, UnhandledExceptionEventArgs e) {
         MessageBox.Show("Log unhandled side-thread-Exception here\n" + 
		e.ExceptionObject.ToString());
         Application.Exit();
      }
   }
}

要对此进行尝试,请编译应用程序并从 IDE 外部直接启动它。

另一个主题:来自辅助线程的事件

看看下面的示例:`CountDown` 类有两个事件,它们在 GUI 线程中引发的方式在我看来是*“壮观地不引人注目”*。

public sealed class CountDown : IDisposable {

   public class TickEventargs : EventArgs {
      public readonly int Counter;
      public TickEventargs(int counter) { this.Counter = counter; }
   }

   public event EventHandler<tickeventargs> Tick = delegate { };
   public event EventHandler IsRunningChanged = delegate { };

   private int _Counter;
   private System.Threading.Timer _Timer;
   private bool _IsRunning = false;

   public CountDown() {
      _Timer = new System.Threading.Timer(Timer_Tick, null, Timeout.Infinite, 1000);
   }

   private void Timer_Tick(object state) {
      Tick.RaiseGui(this, new TickEventargs(_Counter)); //raise event
      if(_Counter == 0) IsRunning = false;
      else _Counter -= 1;
   }

   public bool IsRunning {
      get { return _IsRunning; }
      set {
         if(_IsRunning == value) return;
         _IsRunning = value;
         if(_IsRunning) _Timer.Change(0, 1000);
         else _Timer.Change(Timeout.Infinite, 1000);
         IsRunningChanged.RaiseGui(this);                  //raise event
      }
   }

   public void Start(int initValue) {
      _Counter = initValue;
      IsRunning = true;
   }

   public void Dispose() { _Timer.Dispose(); }
}</tickeventargs>

(`.RaiseGui` 是 `AsyncWorkerX` 发布的第三个扩展方法,VB.NET 中不可用。)

在 VB.NET 中,您也可以将事件引发到 GUI 线程,但您需要按照 Microsoft 的事件设计指南进行设计:即发布一个 `Protected Sub OnXYEvent()`,它会引发事件。调用该 `Sub` 可以轻松地将它像任何其他方法一样传输到 GUI 线程。

   Public Class TickEventargs
      Inherits EventArgs
      Public ReadOnly Counter As Integer
      Public Sub New(ByVal counter As Integer)
         Me.Counter = counter
      End Sub
   End Class

   Public Event Tick As EventHandler(Of TickEventargs)
   Protected Sub OnTick(ByVal e As TickEventargs)
      RaiseEvent Tick(Me, e)
   End Sub

   Private Sub Timer_Tick(ByVal state As Object)
      'raise Tick-event in Gui-thread
      AsyncWorkerX.NotifyGui(AddressOf OnTick, New TickEventargs(_Counter))
      If _Counter = 0 Then
         IsRunning = False
      Else
         _Counter -= 1
      End If
   End Sub

   '...

它不像 C# 中那样优雅,但我认为足够优雅了。

AsyncWorker 的工作原理

`NotifyGui()` 和 `RunAsync()` 的工作方式大不相同,尽管它们的签名设计相同。

RunAsync()

`RunAsync()` 在执行 `Delegate.BeginInvoke()` 之前引发 `IsRunningChanged()`。`Delegate.BeginInvoke()` 以回调作为参数,该回调在辅助线程结束之前由其调用。在此回调中执行 `Delegate.EndInvoke()`,这很重要,原因如下——请参考 **异步方法调用** [ . ]。
最后,将 `IsRunningChanged()` 引发到 GUI 线程,因为回调仍然在辅助线程中运行。
我将展示 `RunAsync()` 的两个重载,一个用于没有参数的方法,另一个用于有两个参数的方法。

public void RunAsync(ActionasyncAction) {
   asyncAction.BeginInvoke( GetCallback( asyncAction.EndInvoke), null);
}
public void RunAsync<T1, T2>(Action<T1, T2> asyncAction, T1 arg1, T2 arg2) {
   asyncAction.BeginInvoke(arg1, arg2, GetCallback( asyncAction.EndInvoke), null);
}

`GetCallback()` 进行一些设置,并返回一个 `AsyncCallback` 委托,该委托将 `.EndInvoke` 调用与 `ToggleIsRunning()` 结合起来,将 `IsRunning` 设置回 `false`。

private AsyncCallback GetCallback(AsyncCallback endInvoke) {
   if(IsRunning) throw this.BugException("I'm already running");
   _CancelRequested = _CancelNotified = false;
   ToggleIsRunning();
   _ReportNext = Environment.TickCount;
   return (AsyncCallback)Delegate.Combine(endInvoke, _ToggleIsRunningCallback);   
}

NotifyGui()

另一方面,`NotifyGui()` 只是将 `syncAction` 及其参数转发给 `TryInvokeGui()`。

/// <summary>
/// if cancelled, returns false. if called once more, throws BugException: 
/// "after cancellation 
/// you should notify Gui no longer.". Use the return-value to avoid that.
/// </summary>
public bool NotifyGui(Action syncAction) { return TryInvokeGui(syncAction, false); }

public bool NotifyGui<T1, T2>(Action<T1, T2> syncAction, T1 arg1, T2 arg2) {
   return TryInvokeGui(syncAction, false, arg1, arg2);
}

`TryInvokeGui()` 负责报告进度以及直接通知 GUI。如果您在取消后调用 GUI,异常会提醒您,否则它会转发给 `InvokeGui()`。
`InvokeGui()` 会在 `Application.OpenForms[0]` 不可用时,或者当 `action.Target` 是一个 `Control` 但已被释放时取消。请参阅 `TryInvokeGui()` 和 `InvokeGui()` 一起查看。

private bool TryInvokeGui(Delegate action, bool reportProgress, 
	params object[] args) {
   if(_CancelRequested) {
      if(_CancelNotified) throw this.BugException(
         "after cancellation you should notify Gui no longer.");
      _CancelNotified = true;
      return false;
   }
   if(reportProgress) {
      var ticks = Environment.TickCount;
      if(ticks < _ReportNext) return true;
      InvokeGui(action, args);
      //set the next reportingtime, aligned to  ReportInterval 
      _ReportNext = ticks + ReportInterval - (ticks - _ReportNext) % ReportInterval;
   } else InvokeGui(action, args);
   return true;
}
private void InvokeGui(Delegate action, params object[] args) {
   var  target = action.Target as Control; 
   if(Application.OpenForms.Count == 0 || 
	(target != null && target.IsDisposed)) Cancel();
   else Application.OpenForms[0].BeginInvoke(action, args);
}

这里的通用技巧是使用 `Application.OpenForms[0]` 作为调用 `Control`,这全局启用了将方法调用传输到 GUI 线程的选项。
这包括独立的类,其中没有可用的 `Control`(如前面显示的 `CountDown` 类)。

历史

  • 2010 年 4 月:添加了关于危险的 `BackgroundWorker` 行为的章节,并更改了 `AsyncWorker` 和演示应用程序的代码。
© . All rights reserved.