這是一個古老的話題。。。直入主題吧!
對winfrom的控制元件來說,多執行緒操作非常容易導致複雜且嚴重的bug,比如不同執行緒可能會因場景需要強制設定控制元件為不同的狀態,進而引起併發、加鎖、死鎖、阻塞等問題。為了避免和解決上述可能出現的問題,微軟要求必須是控制元件的建立執行緒才能操作控制元件資源,其它執行緒不允許直接操作控制元件。但是現代應用又不是單執行緒應用,無論如何肯定會存在其它執行緒需要更新控制元件的需求,於是微軟兩種方案來解決相關問題:InvokeRequired方案和BackgroundWorker方案。
演示程式效果圖和原始碼
檢視程式碼
using System.ComponentModel;
using System.Diagnostics;
using System.Timers;
using Tccc.DesktopApp.WinForms1.BLL;
namespace Tccc.DesktopApp.WinForms1
{
public partial class UIUpdateDemoForm : Form
{
/// <summary>
///
/// </summary>
public UIUpdateDemoForm()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
}
#region 演示InvokeRequired
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void invokeRequiredBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "invokeRequiredBtn_Click 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
new Thread(() =>
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "BeginWorking_Invoke 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
BLLWorker.BeginWorking_Invoke(this, "some input param");
}).Start();
}
/// <summary>
///
/// </summary>
public void UpdatingProgress(int progress)
{
if (this.InvokeRequired)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "InvokeRequired=true 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
this.Invoke(new Action(() =>
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "Sleep2秒 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);//模擬UI操作慢
UpdatingProgress(progress);
}));
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "after Invoke 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
}
else
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "InvokeRequired=false 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
richTextBox1.Text += DateTime.Now.ToString("HH:mm:ss") + ":執行進度" + progress + "%" + Environment.NewLine;
}
}
#endregion
#region 演示BackgroundWorker
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void bgWorkerBtn_Click(object sender, EventArgs e)
{
new Thread(() =>
{
//Control.CheckForIllegalCrossThreadCalls = true;
//richTextBox1.Text = "可以了?";
}).Start();
Debug.WriteLine("bgWorkerBtn_Click 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
if (!backgroundWorker1.IsBusy)
{
richTextBox1.Text = String.Empty;
backgroundWorker1.RunWorkerAsync("hello world");//
}
}
private void bgWorkerCancelBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine("bgWorkerCancelBtn_Click 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
if (backgroundWorker1.IsBusy)
{
backgroundWorker1.CancelAsync();//
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Debug.WriteLine("backgroundWorker1_DoWork 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
BLLWorker.BeginWorking(sender, e);//控制元件遍歷傳遞到業務處理程式中
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Debug.WriteLine("backgroundWorker1_ProgressChanged 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":執行進度" + e.ProgressPercentage + "%";
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Debug.WriteLine("backgroundWorker1_RunWorkerCompleted 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
if (e.Cancelled)
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":已取消";
}
else if (e.Error != null)
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":發生錯誤:" + e.Error.Message;
}
else
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":執行完成";
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":執行結果=" + e.Result;
}
}
#endregion
}
public class BLLWorker
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void BeginWorking_Invoke(UIUpdateDemoForm form, string inputData)
{
int counter = 0;
int max = 5;
while (counter < max)
{
System.Threading.Thread.Sleep(200);
counter++;
form.UpdatingProgress(counter * 20);
}
}
/// <summary>
/// 模擬耗時操作(下載、批量操作等)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void BeginWorking(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
Debug.WriteLine("inputArgument=" + e.Argument as string);
for (int i = 1; i <= 10; i++)
{
if (worker.CancellationPending == true)//檢測是否被取消
{
e.Cancel = true;
break;
}
else
{
// Perform a time consuming operation and report progress.
System.Threading.Thread.Sleep(200);
worker.ReportProgress(i * 10);
}
}
e.Result = "result xxxx";
}
}
}
InvokeRequired方案
上述程式碼中,this.InvokeRequired屬性就是用來判斷當前執行緒和this控制元件的建立執行緒是否一致。
- 當其值=false時,代表當前執行執行緒就是控制元件的建立執行緒,可以直接操作控制元件。
- 當其值=true時,代表當前執行緒不是控制元件的建立執行緒,需要呼叫Invoke方法來實現操作控制元件。
問題來了,呼叫Invoke()怎麼就能實現操作控制元件了呢?我們在演示程式中的UpdatingProgress()增加了詳細的記錄,除錯輸出如下:
16:47:44.907:invokeRequiredBtn_Click 執行緒ID=1
16:47:44.924:BeginWorking_Invoke 執行緒ID=11
16:47:45.133:InvokeRequired=true 執行緒ID=11
16:47:45.139:Sleep2秒 執行緒ID=1
16:47:47.144:InvokeRequired=false 執行緒ID=1
16:47:47.159:after Invoke 執行緒ID=11
16:47:47.363:InvokeRequired=true 執行緒ID=11
16:47:47.371:Sleep2秒 執行緒ID=1
16:47:49.392:InvokeRequired=false 執行緒ID=1
16:47:49.407:after Invoke 執行緒ID=11
16:47:49.622:InvokeRequired=true 執行緒ID=11
16:47:49.628:Sleep2秒 執行緒ID=1
16:47:51.638:InvokeRequired=false 執行緒ID=1
16:47:51.642:after Invoke 執行緒ID=11
16:47:51.857:InvokeRequired=true 執行緒ID=11
16:47:51.863:Sleep2秒 執行緒ID=1
16:47:53.880:InvokeRequired=false 執行緒ID=1
16:47:53.888:after Invoke 執行緒ID=11
16:47:54.099:InvokeRequired=true 執行緒ID=11
16:47:54.104:Sleep2秒 執行緒ID=1
16:47:56.118:InvokeRequired=false 執行緒ID=1
16:47:56.126:after Invoke 執行緒ID=11
結合程式與執行日誌,可以得到以下結論:
- 首先,在Invoke()方法前是執行緒11在執行,Invoke()內的程式碼就變成執行緒1在執行了,說明此處發生了執行緒切換。這也是Invoke()的核心作用:切換到UI執行緒(1號)來執行Invoke()內部程式碼。
- after Invoke日誌的執行緒ID=11,說明Invoke()執行結束後,還是由之前的執行緒繼續執行後續程式碼。
- after Invoke操作的日誌時間顯示是1號執行緒睡眠2秒後執行的,說明Invoke()執行期間,其後的程式碼是被阻塞的。
- 最後,通過程式總耗時來看,由於操作控制元件都需要切換為UI執行緒來執行,因此UI執行緒執行的程式碼中一旦有耗時的操作(比如本例的Sleep),將直接阻塞後續其它的操作,同時伴隨著客戶端程式介面的響應卡頓現象。
BackgroundWorker方案
BackgroundWorker是一個隱形的控制元件,這是微軟封裝程度較高的方案,它使用事件驅動模型。
演示程式的日誌輸出為:
bgWorkerBtn_Click 執行緒ID=1
backgroundWorker1_DoWork 執行緒ID=4
inputArgument=hello world
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_ProgressChanged 執行緒ID=1
backgroundWorker1_RunWorkerCompleted 執行緒ID=1
通過日誌同樣可以看出:
- 其中DoWork事件的處理程式(本例的backgroundWorker1_DoWork)用來執行耗時的業務操作,該部分程式碼由後臺執行緒執行,而非UI執行緒,也正因此,backgroundWorker1_DoWork程式碼中就無法操作控制元件資源。
- BackgroundWorker的其它幾個事件處理程式,如backgroundWorker1_ProgressChanged、backgroundWorker1_RunWorkerCompleted,就都由UI執行緒來執行,因此也就可以直接操作控制元件資源。
補充1:BackgroundWorker的ReportProgress(int percentProgress, object? userState)過載方法,其中userState引數可以承載percentProgress之外的一些有用資訊。在scanningWorker_ProgressChanged中通過e.UserState接收並。
補充2:雖然RunWorkerCompletedEventArgs型別定義了UserState屬性,但是其值始終為null,因此在RunWorkerCompleted事件處理程式中需要用e.Result來傳遞"結果"資料。
通過原始碼可以看到UserState沒有賦值。
Control.CheckForIllegalCrossThreadCalls是咋回事?
官方註釋:
Gets or sets a value indicating whether to catch calls on the wrong thread
that access a control's System.Windows.Forms.Control.Handle property
when an application is being debugged.
When a thread other than the creating thread of a control tries to access one of that control's methods or properties, it often leads to unpredictable results. A common invalid thread activity is a call on the wrong thread that accesses the control's Handle property. Set CheckForIllegalCrossThreadCalls to true
to find and diagnose this thread activity more easily while debugging.
通俗理解:
雖然微軟不建議其它執行緒操作控制元件,但是如果就這麼寫了,程式也能執行,比如下面的情況:
private void invokeRequiredBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + ":invokeRequiredBtn_Click 執行緒ID=" + Thread.CurrentThread.ManagedThreadId);
new Thread(() =>
{
richTextBox1.Text = DateTime.Now.ToString();
}).Start();
}
而Control.CheckForIllegalCrossThreadCalls這個屬性就是用來設定,是否完全禁止跨執行緒的操作控制元件。當設定true,上述操作就完全不能執行了。
注意:在VS中F5除錯時,此值預設=true。報錯效果:
雙擊生成的exe執行時,此值預設=false,程式還可以執行。當Control.CheckForIllegalCrossThreadCalls設定為true時,雙擊exe執行程式會異常退出:
建議:如果是新開發的程式,建議設定為true,可以及早的發現隱患問題,避免程式複雜後需要付出高昂的分析成本。
總結
以上是winform開發的基礎中的基礎,本文在系統的查閱微軟文件的基礎上,通過演示程式推測和驗證相關的邏輯關係。
同時聯想到:由於控制元件的更新都需要UI執行緒來執行,因此當遇到程式客戶端程式響應卡頓/卡死的情況,通過dump分析UI執行緒的堆疊,應該可以有所發現。