本文翻译自Sacha Barber的文章 Beginners Guide to Threading in .NET: Part 5 of n。
这个系列包括5篇文章,这是最后一篇。文章由浅入深,介绍了有关多线程的方方面面,很不错。
1)Why Thread UIs
应用程序如果出现UI反应迟钝,这个时候就可能考虑线程与UI的关系了。如何避免这种情况,那就是让后台任务在后台线程运行,只留下UI来应对用户的操作。
当后台线程工作完成时,我们允许它在合适的时间更新UI。
这篇文件主要介绍创建UI使之与单个或多个线程协作,来保证UI的灵敏度,也就是UI不会卡死。文章主要涉及WinForm,WPF及使用Silverlight的关键点。
2)Threading in WinForms
这一部分将会介绍在如何在WinForms环境下使用线程,涉及到.Net 2.0之后才出现的BackgroundWorker。这是目前为止使用最简单的方式创建和控制后台线程并与UI协同。当然,我们不会涉及很多创建和控制线程相关的便利,只关注与UI的协同。
BackgroundThread使用并不像你创建自己的线程那样便利,它一般使用与以下特殊场合:
a)后台工作
b)带参数的后台应用
c)显示进展·
d)报告结束
e)可以取消的
如果这些都满足你的要求,那选择BackgroundThread那就对了。当然,对于其精妙的控制,那就要靠我们自己了。这篇文章将会使用BackgroundWorker来完成后台任务,至于有关线程更多信息,你可以查找本系列的其它文章。
A)一个不好的实例
界面上就一个按钮加textBox,运行代码,将会看到
当然,线程结束时我们是能看到提示的:Completed background task。
错误提示很显然是在后台线程中我们访问了UI线程的txtResults。下面为我们使用的代码:
public partial class BackgroundWorkerBadExample : Form { public BackgroundWorkerBadExample() { InitializeComponent(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { try { for (int i = 0; i < (int)e.Argument; i++) { txtResults.Text += string.Format("processing {0}\r\n", i.ToString()); } } catch (InvalidOperationException oex) { MessageBox.Show(oex.Message); } } private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { MessageBox.Show("Completed background task"); } private void btnGo_Click(object sender, EventArgs e) { backgroundWorker1.RunWorkerAsync(100); } }
我们重点关注backgroundWorker1_DoWork()方法,在这里我们得到了一个异常InvalidOperationException,这是由于,在.Net编程环境,有一项基本原则就是所有的控件只能由创建它的用户接触。在这个实例中,在后台线程完成时我们没有指派相关操作来应对此事,直接使用的话就会出现令人讨厌的异常。
我们可以使用几种方式来修复这种异常,在这之前,我们先来讲讲BackgroundWorker:
Task |
What needs Setting |
Report Progress | WorkerReportProgress = True, and wire up the ProgressChangedEvent |
Support Cancelling | WorkerSupportsCancellation = True |
Running without Param | None |
Running with Param | None |
B)一些好的方式
方式1:使用BeginInvoke(适用于所有版本的.Net)
try { for (int i = 0; i < (int)e.Argument; i++) { if (this.InvokeRequired) { this.Invoke(new EventHandler(delegate { txtResults.Text += string.Format("processing {0}\r\n", i.ToString()); })); } else txtResults.Text += string.Format("processing {0}\r\n", i.ToString()); } } catch (InvalidOperationException oex) { MessageBox.Show(oex.Message); }
这可以看做更新UI最古老的方式。
方式2:使用同步上下文(SynchronizationContext;适用于.Net 2.0及以上)
private SynchronizationContext context;
.....
.....
//set up the SynchronizationContext
context = SynchronizationContext.Current;
if (context == null)
{
context = new SynchronizationContext();
}
.....
.....
try
{
for (int i = 0; i < (int)e.Argument; i++)
{
context.Send(new SendOrPostCallback(delegate(object state)
{
txtResults.Text += string.Format(
"processing {0}\r\n", i.ToString());
}), null);
}
}
catch (InvalidOperationException oex)
{
MessageBox.Show(oex.Message);
}
SynchronizationContext对象允许我们通过Send()方法来操作UI线程。在其内部只是包装了一些匿名委托。
方式3:使用Lambdas(适用于.Net3.0及以上)
private SynchronizationContext context;
.....
.....
//set up the SynchronizationContext
context = SynchronizationContext.Current;
if (context == null)
{
context = new SynchronizationContext();
}
.....
.....
try
{
for (int i = 0; i < (int)e.Argument; i++)
{
context.Send(new SendOrPostCallback((s) =>
txtResults.Text += string.Format(
"processing {0}\r\n", i.ToString())
), null);
}
}
catch (InvalidOperationException oex)
{
MessageBox.Show(oex.Message);
}
使用lambda来代替匿名委托。lambda适用于小任务,但偶有人用于较复杂的工作。
下面是正常运行时的界面:
C)有关更新进度部分
实例代码界面上包括按钮Go,按钮Cancel及文本框txtResults,代码如下:
private int factor = 0; private SynchronizationContext context; public BackgroundWorkerReportingProgress() { InitializeComponent(); //set up the SynchronizationContext context = SynchronizationContext.Current; if (context == null) { context = new SynchronizationContext(); } } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; try { for (int i = 0; i < (int)e.Argument; i++) { if (worker.CancellationPending) { e.Cancel = true; return; } context.Send(new SendOrPostCallback( (s) => txtResults.Text += string.Format( "processing {0}\r\n", i.ToString()) ), null); //report progress Thread.Sleep(1000); worker.ReportProgress((100 / factor) * i + 1); } } catch (InvalidOperationException oex) { MessageBox.Show(oex.Message); }
}
private void btnGo_Click(object sender, EventArgs e) { factor = 100; backgroundWorker1.RunWorkerAsync(factor); } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar1.Value = e.ProgressPercentage; } private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { MessageBox.Show("Completed background task"); } private void btnCancel_Click(object sender, EventArgs e) { backgroundWorker1.CancelAsync(); }
代码中在BackgroundWorker.DoWork中报告进展,在ProgressChanged事件中更新进度条的值(这个时候为啥能更新界面,不明白?)。
3)Threading in WPF
WPF出现于.Net 3.0之后,虽然界面编程方式不同,但线程操作部分是相同的。’控件只能由创建的线程来接触‘原来同样适用于WPF。唯一不同的是我们必须使用一个WPF对象Dispatcher,后者管理线程上的工作队列。
以下代码是在WPF中使用BackgroundWorker,XAML代码部分没有显示:
public partial class BackGroundWorkerWindow : Window { private BackgroundWorker worker = new BackgroundWorker(); public BackGroundWorkerWindow() { InitializeComponent(); //Do some work with the Background Worker that //needs to update the UI. //In this example we are using the System.Action delegate. //Which encapsulates a a method that takes no params and //returns no value. //Action is a new in .NET 3.5 worker.DoWork += (s, e) => { try { for (int i = 0; i < (int)e.Argument; i++) { if (!txtResults.CheckAccess()) { Dispatcher.Invoke(DispatcherPriority.Send, (Action)delegate { txtResults.Text += string.Format( "processing {0}\r\n", i.ToString()); }); } else txtResults.Text += string.Format("processing {0}\r\n", i.ToString()); } } catch (InvalidOperationException oex) { MessageBox.Show(oex.Message); } }; } private void btnGo_Click(object sender, RoutedEventArgs e) { worker.RunWorkerAsync(100); } }
这与上面的WinForm实例相似,区别在于Dispatcher,CheckAccess()类似于WinForm中的InvokeRequired;另外需要注意的是把一个委托打包成System.Action,这个委托包括了一个无参无返回值的方法。下面是关键部分的对照:
//WPF
if (!txtResults.CheckAccess()) { Dispatcher.Invoke(DispatcherPriority.Send, (Action)delegate { txtResults.Text += string.Format("processing {0}\r\n", i.ToString()); }); } else txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
//WinForm
if (this.InvokeRequired) { this.Invoke(new EventHandler(delegate { txtResults.Text += string.Format("processing {0}\r\n", i.ToString()); })); } else txtResults.Text += string.Format("processing {0}\r\n", i.ToString());
以上介绍了WPF中使用backgroundWorker,接下来介绍使用使用线程池来实现.
方式1:使用Lambdas
try { for (int i = 0; i < 10; i++) { //CheckAccess(), which is rather strangely marked [Browsable(false)] //checks to see if an invoke is required //and where i respresents the State passed to the //WaitCallback if (!txtResults.CheckAccess()) { //use a lambda, which represents the WaitCallback //required by the ThreadPool.QueueUserWorkItem() method ThreadPool.QueueUserWorkItem(waitCB => { int state = (int)waitCB; Dispatcher.BeginInvoke(DispatcherPriority.Normal, ((Action)delegate { txtResults.Text += string.Format( "processing {0}\r\n", state.ToString()); })); }, i); } else txtResults.Text += string.Format( "processing {0}\r\n", i.ToString()); } } catch (InvalidOperationException oex) { MessageBox.Show(oex.Message); }
由于涉及到System.Action,因此需在.Net 3.5及以上实现。代码中最为主的是取得WaitCallback的状态,状态参数通常是个object,使用lambda可以减小代码量。上面的waitCB就是实际的状态对象(Object类型),所以需要拆包操作。WaitCallback最常用的方式是以下方式二。
方式2:更加明了的语法
try { for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc), i); } } catch (InvalidOperationException oex) { MessageBox.Show(oex.Message); } .... .... .... // This is called by the ThreadPool when the queued QueueUserWorkItem // is run. This is slightly longer syntax than dealing with the Lambda/ // System.Action combo. But it is perhaps more readable and easier to // follow/debug private void ThreadProc(Object stateInfo) { //get the state object int state = (int)stateInfo; //CheckAccess(), which is rather strangely marked [Browsable(false)] //checks to see if an invoke is required if (!txtResults.CheckAccess()) { Dispatcher.BeginInvoke(DispatcherPriority.Normal, ((Action)delegate { txtResults.Text += string.Format( "processing {0}\r\n", state.ToString()); })); } else txtResults.Text += string.Format( "processing {0}\r\n", state.ToString()); }
个人比较喜欢方式二,直观。
4)Threading in Silverlight
需要安装Silverlight 2.0 beta版。(略)