WinForms 已經開源,您如今能夠在 GitHub 上查看 WinForm 源代碼。git
正好有人又討論到在 WinFroms 環境下的多線程開發,這裏就再整理一下涉及到的技術點。github
從官方文檔能夠知道,Windows Forms 是 Windows 界面庫,例如 User32 和 GDI+ 的 .NET 封裝,WinForms 中的控件背後其實是 Windows 的 GDI 控件實現。windows
LongTimeWork 表明一個須要長時間操做才能完成的任務。這裏經過 Sleep() 來模擬長事件的操做。api
主要特性:安全
代碼以下:多線程
using System; using System.Collections.Generic; using System.Text; namespace LongTime.Business { // 定義事件的參數類 public class ValueEventArgs: EventArgs { public int Value { set; get; } } // 定義事件使用的委託 public delegate void ValueChangedEventHandler(object sender, ValueEventArgs e); public class LongTimeWork { // 定義一個事件來提示界面工做的進度 public event ValueChangedEventHandler ValueChanged; // 報告任務被取消 public event EventHandler Cancelled; public event EventHandler Finished; // 觸發事件的方法 protected void OnValueChanged(ValueEventArgs e) { this.ValueChanged?.Invoke(this, e); } public void LongTimeMethod(System.Threading.CancellationTokenSource cancellationTokenSource) { for (int i = 0; i < 100; i++) { if(cancellationTokenSource.IsCancellationRequested) { this.Cancelled?.Invoke(this, EventArgs.Empty); return; } // 進行工做 System.Threading.Thread.Sleep(1000); // 觸發事件 ValueEventArgs e = new ValueEventArgs() { Value = i + 1 }; this.OnValueChanged(e); } this.Finished?.Invoke(this, EventArgs.Empty); } } }
Control 基類 是 WinForms 中控件的基類,它定義了控件顯示給用戶的基礎功能,須要注意的是 Control 是一個 .NET 中的類,咱們建立出來的也是 .NET 對象實例。可是當控件真的須要在 Windows 上工做的時候,它必需要建立爲一個實際的 GDI 控件,當它實際建立以後,能夠經過 Control 的 Handle 屬性提供 Windows 的窗口句柄。異步
new 一個 .NET 對象實例並不意味着實際的 GDI 對象被建立,例如,當執行到窗體的構造函數的時候,這時候僅僅正在建立 .NET 對象,而窗體所依賴的 GDI 對象尚未被處理,也就意味着真正的控件實際上尚未被建立出來,咱們也就不能開始使用它,這就是 IsHandleCreated 屬性的做用。函數
須要說明的是,一般咱們並不須要管理底層的 GDI 處理,WinForms 已經作了良好的封裝,咱們須要知道的是關鍵的時間點。ui
構造函數是面向對象中的概念,執行構造函數的時候,說明正在內存中構建對象實例。而窗體的 Load 事件發生在窗體建立以後,與窗體第一次顯示在 Windows 上以前的時間點上。this
它們的關鍵區別在於窗體背後所對應的 GDI 對象建立問題。在構造函數執行的時候,背後對應的 GDI 對象尚未被建立,因此,咱們並不能訪問窗體以及控件。在 Load 事件執行的時候,GDI 對象已經建立,因此能夠訪問窗體以及控件。
在使用多線程模式開發 WinForms 窗體應用程序的時候,須要保證後臺線程對窗體和控件的訪問在 Load 事件以後進行。
Windows 窗體中的控件是綁定到特定線程的,不是線程安全的。 所以,在多線程狀況下,若是從其餘線程調用控件的方法,則必須使用控件的一個調用方法將調用封送到正確的線程。
當你在窗體的按鈕上,經過雙擊生成一個對應的 Click 事件處理方法的時候,這個事件處理方法其實是執行在這個特定的 UI 線程之上的。
不過 UI 線程背後的機制與 Windows 的消息循環直接相關,在 UI 線程上執行長時間的代碼會致使 UI 線程的阻塞,直接表現就是界面卡頓。解決這個問題的關鍵是在 UI 線程以外的工做線程上執行須要花費長時間執行的任務。
這個時候,就會涉及到 UI 線程安全問題,在 工做線程上是不能直接訪問 UI 線程上的控件,不然,會致使異常。
那麼工做線程如何更新 UI 界面上的控件以達到更新顯示的效果呢?
UI 控件提供了一個能夠安全訪問的屬性:
和 4 個能夠跨線程安全訪問的方法:
不要被這些名字所迷惑,咱們從線程的角度來看它們的做用。
InvokeRequired 用來檢查當前的線程是否就是建立控件的線程,如今 WinForms 已經開源,你能夠在 GitHub 上查看 InvokeRequired 源碼,最關鍵的就是最後的代碼行。
public bool InvokeRequired { get { using var scope = MultithreadSafeCallScope.Create(); Control control; if (IsHandleCreated) { control = this; } else { Control marshalingControl = FindMarshalingControl(); if (!marshalingControl.IsHandleCreated) { return false; } control = marshalingControl; } return User32.GetWindowThreadProcessId(control, out _) != Kernel32.GetCurrentThreadId(); } }
因此,咱們能夠經過這個 InvokeRequired 屬性來檢查當前的線程是不是 UI 的線程,若是是的話,才能夠安全訪問控件的方法。示例代碼以下:
if (!this.progressBar1.InvokeRequired) { this.progressBar1.Value = e.Value; }
可是,若是當前線程不是 UI 線程呢?
當在工做線程上須要訪問控件的時候,關鍵點在於咱們不能直接調用控件的 4 個安全方法以外的方法。這時候,必須將須要執行的操做封裝爲一個委託,而後,將這個委託經過 Invoke() 方法投遞到 UI 線程之上,經過回調方式來實現安全訪問。
這個 Invoke() 方法的定義以下:
public object Invoke (Delegate method); public object Invoke (Delegate method, params object[] args);
這個 Delegate 其實是全部委託的基類,咱們使用 delegate 定義出來的委託都是它的派生類。這就意味全部的委託其實都是可使用的。
不過,有兩個特殊的委託被推薦使用,根據微軟的文檔,它們比使用其它類型的委託速度會更快。見:https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invoke?view=net-5.0
當註冊的委託被系統回調的時候,若是委託類型是 EventHandler,那麼參數 sender 將被設置爲控件自己的引用,而 e 的值是 EventArgs.Empty。
MethodInvoder 委託的定義以下,能夠看到它與 Action 委託定義其實是同樣的,沒有參數,返回類型爲 void。
public delegate void MethodInvoker();
因爲須要確保對控件的訪問在 UI 線程上執行,建立輔助方法進行處理。
這裏的 this 就是 Form 窗體自己。
private void SafeInvoke(System.Windows.Forms.MethodInvoker method) { if (this.InvokeRequired) { this.Invoke(method); } else { method(); } }
這樣在須要訪問 UI 控件的時候,就能夠經過這個 SafeInvode() 來安全操做了。
private void workder_ValueChanged(object sender, ValueEventArgs e) { this.SafeInvoke( () => this.progressBar1.Value = e.Value ); }
若是你查看 BeginInvoke() 的源碼,能夠發現它與 Invoke() 方法的代碼幾乎相同。
public object Invoke(Delegate method, params object[] args) { using var scope = MultithreadSafeCallScope.Create(); Control marshaler = FindMarshalingControl(); return marshaler.MarshaledInvoke(this, method, args, true); }
BeginInvoke() 方法源碼
public IAsyncResult BeginInvoke(Delegate method, params object[] args) { using var scope = MultithreadSafeCallScope.Create(); Control marshaler = FindMarshalingControl(); return (IAsyncResult)marshaler.MarshaledInvoke(this, method, args, false); }
它們都會保證註冊的委託運行在 UI 安全的線程之上,區別在於使用 BeginInvoke() 方法的場景。
若是你的委託內部使用了異步操做,而且返回一個處理異步的 IAsyncResult,那麼就使用 BeginInvoke()。之後,使用 EndInvode() 來獲得這個異步的返回值。
在 .NET 中,使用線程並不意味着必定要建立 Thread 對象實例,咱們能夠經過系統提供的線程池來使用線程。
線程池提供了將一個委託註冊到線程池隊列中的方法,該方法要求的委託類型是 WaitCallback。
public static bool QueueUserWorkItem (System.Threading.WaitCallback callBack); public static bool QueueUserWorkItem<TState> (Action<TState> callBack, TState state, bool preferLocal);
WaitCallback 委託的定義,它接收一個參數對象,返回類型是 void。
public delegate void WaitCallback(object state);
能夠將啓動工做線程的方法修改成以下方式,這裏使用了棄元操做,見 棄元 - C# 指南。
System.Threading.WaitCallback callback = _ => worker.LongTimeMethod(); System.Threading.ThreadPool.QueueUserWorkItem(callback);
完整的代碼:
using LongTime.Business; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { } private void Button1_Click(object sender, EventArgs e) { // 禁用按鈕 this.button1.Enabled = false; // 實例化業務對象 LongTime.Business.LongTimeWork worker = new LongTime.Business.LongTimeWork(); worker.ValueChanged += new LongTime.Business.ValueChangedEventHandler(workder_ValueChanged); /* // 建立工做線程實例 System.Threading.Thread workerThread = new System.Threading.Thread(worker.LongTimeMethod); // 啓動線程 workerThread.Start(); */ System.Threading.WaitCallback callback = _ => worker.LongTimeMethod(); System.Threading.ThreadPool.QueueUserWorkItem(callback); } private void SafeInvoke(System.Windows.Forms.MethodInvoker method) { if (this.InvokeRequired) { this.Invoke(method); } else { method(); } } private void workder_ValueChanged(object sender, ValueEventArgs e) { this.SafeInvoke( () => this.progressBar1.Value = e.Value ); } } }
BackgroundWorker 封裝了 WinForms 應用程序中,在 UI 線程以外的工做線程vs執行任務的處理。
主要特性:
該控件實際上但願你將業務邏輯直接寫在它的 DoWork 事件處理中。可是,實際開發中,咱們可能更但願將業務寫在單獨的類中實現。
咱們直接使用 BackgroundWorker 的特性來完成。
首先,報告進度要進行兩個基本設置:
// 設置報告進度 this.backgroundWorker1.WorkerReportsProgress = true; // 註冊進度更新的事件回調 backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler( backgroundWorker1_ProgressChanged);
當後臺任務發生更新以後,經過調用 BackgroundWorker 的 ReportProgress() 方法來報告進度,這個一個線程安全的方法。
而後,BackgroundWorker 的 ProgressChanged 事件會被觸發,它會運行在 UI 線程之上,能夠安全的操做控件的方法。
private void workder_ValueChanged(object sender, ValueEventArgs e) { // 經過 BackgroundWorker 來更新進度 this.backgroundWorker1.ReportProgress( e.Value); } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { // BackgroundWorker 能夠安全訪問控件 this.progressBar1.Value = e.ProgressPercentage; }
因爲咱們並不在 DoWork 事件中實現業務,因此也不使用 BackgroundWorker 的報告完成操做。
在業務代碼中,提供任務完成的事件。
this.Finished?.Invoke(this, EventArgs.Empty);
在窗體中,註冊事件回調處理,因爲回調處理不能保證執行在 UI 線程之上, 經過委託將待處理的 UI 操做封裝爲委託對象傳遞給 SaveInfoke() 方法。
private void worker_Finished(object sender, EventArgs e) { SafeInvoke(() => { this.Reset(); this.resultLabel.Text = "Task Finished!"; }); }
BackgroundWorker 的取消是創建在整個業務處理寫在 DoWork 事件回調中, 咱們的業務寫在獨立的類中。因此,咱們本身完成對於取消的支持。
讓咱們的處理方法接收一個 對象來支持取消。每次都從新建立一個新的取消對象。
// 每次從新構建新的取消通知對象 this.cancellationTokenSource = new System.Threading.CancellationTokenSource(); worker.LongTimeMethod( this.cancellationTokenSource);
點擊取消按鈕的時候,發出取消信號。
private void BtnCancel_Click(object sender, EventArgs e) { // 發出取消信號 this.cancellationTokenSource.Cancel(); }
業務代碼中會檢查是否收到取消信號,收到取消信號會發出取消事件,並退出操做。
if(cancellationTokenSource.IsCancellationRequested) { this.Cancelled?.Invoke(this, EventArgs.Empty); return; }
在窗體註冊的取消事件處理中,處理取消響應,仍是須要注意線程安全問題
private void worker_Cancelled(object sender, EventArgs e) { SafeInvoke(() => { this.Reset(); this.resultLabel.Text = "Task Cancelled!"; }); }
using LongTime.Business; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { private System.ComponentModel.BackgroundWorker backgroundWorker1; private System.Threading.CancellationTokenSource cancellationTokenSource; public Form1() { InitializeComponent(); // 建立後臺工做者對象實例 this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); // 設置報告進度 this.backgroundWorker1.WorkerReportsProgress = true; // 支持取消操做 this.backgroundWorker1.WorkerSupportsCancellation = true; // 註冊開始工做的事件回調 backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); // 註冊進度更新的事件回調 backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { // 能夠接收來自 RunWorkerAsync() 的參數,供實際的方法使用 object argument = e.Argument; // 後臺進程,不能訪問控件 // 實例化業務對象 LongTime.Business.LongTimeWork worker = new LongTime.Business.LongTimeWork(); worker.ValueChanged += new LongTime.Business.ValueChangedEventHandler(workder_ValueChanged); worker.Finished += new EventHandler(worker_Finished); worker.Cancelled += new EventHandler(worker_Cancelled); // 每次從新構建新的取消通知對象 this.cancellationTokenSource = new System.Threading.CancellationTokenSource(); worker.LongTimeMethod(this.cancellationTokenSource); } private void worker_Cancelled(object sender, EventArgs e) { SafeInvoke(() => { this.Reset(); this.resultLabel.Text = "Task Cancelled!"; }); } private void worker_Finished(object sender, EventArgs e) { SafeInvoke(() => { this.Reset(); this.resultLabel.Text = "Task Finished!"; }); } private void SafeInvoke(System.Windows.Forms.MethodInvoker method) { if (this.InvokeRequired) { this.Invoke(method); } else { method(); } } private void Form1_Load(object sender, EventArgs e) { } private void Button1_Click(object sender, EventArgs e) { // 控件操做,禁用按鈕 this.button1.Enabled = false; this.button2.Enabled = true; // 啓動後臺線程工做 // 實際的工做註冊在 this.backgroundWorker1.RunWorkerAsync(); } private void workder_ValueChanged(object sender, ValueEventArgs e) { // 經過 BackgroundWorker 來更新進度 this.backgroundWorker1.ReportProgress(e.Value); } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { // BackgroundWorker 能夠安全訪問控件 this.progressBar1.Value = e.ProgressPercentage; } private void BtnCancel_Click(object sender, EventArgs e) { // 發出取消信號 this.cancellationTokenSource.Cancel(); } private void Reset() { this.resultLabel.Text = string.Empty; // Enable the Start button. this.button1.Enabled = true; // Disable the Cancel button. this.button2.Enabled = false; this.progressBar1.Value = 0; } } }
詳細的示例能夠參看微軟 Docs 文檔中的 BackgroundWorker 類。