在文本框中輸入一個數字,點擊開始累加按鈕,程序計算從1開始累計到該數字的結果。因為該累加過程比較耗時,如果直接在UI線程中進行,那么當前窗口將出現假死。為了有更好的用戶體驗,程序啟動一個新的線程來單獨執行該計算,然后每隔200毫秒讀取一次累加結果,並把結果顯示到文本框下方的label控件中。同時,程序支持取消操作,點擊取消累計按鈕,程序將取消累加操作,並把當前累加值顯示到label中。為了方便后面的描述,我把UI線程稱作主線程,把執行累加計算的線程稱作工作者線程。該過程有兩個關鍵點:
1:如何在工作者線程中訪問主線程創建的控件;
2:如何取消比較耗時的計算;
為了便於在工作者線程中調用累加過程,我把它寫成一個單獨方法,如下:
/// <summary> /// 從1累加到指定的值,為了讓該方法支持取消操作所以需要CancellationToken參數 /// </summary> /// <param name="countTo">累加到的指定值</param> /// <param name="ct">取消憑證</param> private void CountTo(int countTo, CancellationToken ct) { int sum = 0; for (; countTo > 0; countTo--) { if (ct.IsCancellationRequested) { break; } sum += countTo; //Invoke方法用於獲得創建lbl_Status的線程所在的上下文 this.Invoke(new Action(()=>lbl_Status.Text = sum.ToString())); Thread.Sleep(200); } }
該方法就是用於累加數字,它有兩個需要注意的地方
1:方法需要傳遞一個CancellationToken參數,用於支持取消操作(《clr via c# 3版》中把這種方式稱作協作式取消,也就是說某一個操作必須支持取消,然后才能取消該操作);
2:為了允許工作者線程訪問主線程創建的lbl_Status控件,我在該線程中使用this.Invoke方法。該方法用於獲得主線程所創建控件的訪問權。它需要一個委托作為參數,在該委托中我們可以定義對lbl_Status的操作。例如在上例中我就是把當前的累加結果賦給lbl_Status的Text屬性。
然后我們看一下如何在一個共走着線程中執行計算耗時的操作,也就是“開始累加”按鈕的操作:
private void btn_Count_Click(object sender, EventArgs e) { _cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(state=>CountTo(int.Parse(txt_CountTo.Text))); }
我使用線程池線程來執行該操作,之所以使用線程池線程而不是自己的Threading對象,是因為線程池是由.NET FrameWork進行維護,默認已經為我們創建好了一些線程,從而省去創建新線程造成的一些列資源消耗,同時,完成計算任務后該線程池線程會自動回到池中等待下一個任務。我把_cts作為一個成員變量,聲明如下:
private CancellationTokenSource _cts;
它需要引入 System.Threading 命名空間。
取消操作更加簡單,代碼如下:
private void btn_Cancel_Click(object sender, EventArgs e) { if (_cts != null) _cts.Cancel(); }
這樣我們就完成了在winform中使用多線程的例子,同時該例子支持取消操作。完整代碼如下:
using System; using System.Threading; using System.Windows.Forms; namespace WinformApp { public partial class Form1 : Form { private CancellationTokenSource _cts; public Form1() { InitializeComponent(); } /// <summary> /// 從1累加到指定的值,為了讓該方法支持取消操作所以需要CancellationToken參數 /// </summary> /// <param name="countTo">累加到的指定值</param> /// <param name="ct">取消憑證</param> private void CountTo(int countTo) { int sum = 0; for (; countTo > 0; countTo--) { if (ct.IsCancellationRequested) { break; } sum += countTo; //Invoke方法用於獲得創建lbl_Status的線程所在的上下文 this.Invoke(new Action(()=>lbl_Status.Text = sum.ToString())); Thread.Sleep(200); } } private void btn_Count_Click(object sender, EventArgs e) { _cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(state=>CountTo(int.Parse(txt_CountTo.Text))); } private void btn_Cancel_Click(object sender, EventArgs e) { if (_cts != null) _cts.Cancel(); } private void btn_Pause_Click(object sender, EventArgs e) { } } }
解決跨線程訪問的問題
主要有兩個方案:
1、關閉跨線程檢查。
2、通過委托的方式,在控件的線程上執行。
具體的代碼如下:
using System; using System.Threading; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form2 : Form { public Form2() { InitializeComponent(); //方法一:不進行跨線程安全檢查 CheckForIllegalCrossThreadCalls = false; } private void button1_Click(object sender, EventArgs e) { Thread th1 = new Thread(new ThreadStart(CalNum)); th1.Start(); } private void CalNum() { SetCalResult(DateTime.Now.Second); } //方法二:檢查是否跨線程,然后將方法加入委托,調用委托 public delegate void SetTextHandler(int result); private void SetCalResult(int result) { if (label2.InvokeRequired == true) { SetTextHandler set = new SetTextHandler(SetCalResult);//委托的方法參數應和SetCalResult一致 label2.Invoke(set, new object[] { result }); //此方法第二參數用於傳入方法,代替形參result } else { label2.Text = result.ToString(); } } } }
改進
在我的Winform程序中,子線程涉及到對多個控件的更改,於是封裝了一下,我這里使用的是拓展方法,只有在.net 3.5上才能支持,如果是.net2.0的環境,需要添加
namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)] public class ExtensionAttribute : Attribute { } }
封裝如下:
using System.Threading; using System.Windows.Forms; namespace WindowsFormsApplication1 { public static class Class1 { /// <summary> /// 跨線程訪問控件 在控件上執行委托 /// </summary> /// <param name="control">控件</param> /// <param name="method">執行的委托</param> public static void CrossThreadCalls(this Control control, ThreadStart method) { if (!control.IsHandleCreated || control.IsDisposed || control.Disposing) { return; } if (method == null) { return; } if (control.InvokeRequired) { control.Invoke(method, null); } else { method(); } } } }
線程池是不可控制的.