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






4.76/5 (21投票s)
泛型方法实现类型安全的线程。
引言
线程通常用于处理耗时过长的进程,就好像你可以等待它一样。尤其是运行 GUI 的线程,必须避免执行耗时的操作,因为用户输入可能随时发生。如果 GUI 线程因某个特定操作忙碌超过大约 100 毫秒,用户会立即感到“卡顿”或“应用程序无响应”。
现在已经(不幸地)过时了
2012 年,Microsoft 推出了 Async/Await 模式,作为当前线程处理的“最新技术”。
与此处发布的方法相比,Async/Await 能够更好地、更令人信服地解决 5 个线程问题(尽管此方法也是一个非常好的方法!)。
因此,请参考我的 Async/Await 文章。
示例应用程序
输入是鼠标点击彩色表面,以及点击发生的时间。输出会将 `Label` 放置在鼠标点击位置,并显示发生时间。通过调用 `Thread.Sleep()` 来模拟耗时的 D 数据评估。
我实现了此行为的多种变体,以演示不同复杂程度的级别。
过程式设计
线程意味着对正常顺序的基本重组。正常顺序可以先评估数据,然后显示它。操作的顺序直接写入代码中。
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` 事件暂时被取消订阅,以抑制一次请求多个并行进程的选项。
线程原则“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` 和演示应用程序的代码。