本文翻譯自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版。(略)