C# 多執行緒詳解 Part.02(UI 執行緒和子執行緒的互動、ProgressBar 的非同步呼叫)
我們先來看一段執行時會丟擲 InvalidOperationException 異常的程式碼段:
private void btnThreadA_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ChangeTextBox);
thread.IsBackground = true;
thread.Start();
}
void ChangeTextBox()
{
for (int i = 0; i < 10000; i++)
{
int num = Int32.Parse(txtNum.Text);
num++;
txtNum.Text = num.ToString();
}
}
微軟在子執行緒修改 UI 執行緒的控制元件值時給出的安全限制方案為:在 VS2005 或者更高版本中,只要不是在控制元件的建立執行緒(一般就是指UI主執行緒)上訪問控制元件的屬性就會丟擲這個錯誤,解決方法就是利用控制元件提供的 Invoke 和 BeginInvoke 把呼叫封送回 UI 執行緒,也就是讓控制元件屬性修改在UI執行緒上執行;或者 禁用此安全限制。
解決方案一:解除該控制元件上對錯誤執行緒呼叫的檢查(謹慎使用)。
publicForm2()
{
InitializeComponent();
// 解除 TextBox 對錯誤執行緒呼叫的檢查
// 如果要捕獲了對錯誤執行緒的呼叫,則為 true(預設值);否則為 false
// 對控制元件許可權可以開放的更大 例如 Control、Form 等
TextBox.CheckForIllegalCrossThreadCalls = false;
}
解決方案二:
void ChangeTextBox(string str)
{
txtNum.Text = str;
}
// 增加一個委託
delegatevoid ChangeTextBoxEventHandler(string str);
// 次迴圈必須在子執行緒上執行,然後將最新值傳遞到 UI 執行緒 文字框才會即時變化
// 如果迴圈寫在 ChangeTextBox 函式中,那麼迴圈真實執行權會交由 UI 執行緒,你只會直接看見結果,看不到過程
void ThreadRun1()
{
for (int i = 0; i < 10000; i++)
{
int num = int.Parse(txtNum.Text);
num++;
// 使用 Invoke 方法,將函式執行勸交回給 UI 執行緒
this.Invoke(new ChangeTextBoxEventHandler(ChangeTextBox), num.ToString());
}
}
private void btnThreadB_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ThreadRun1);
thread.IsBackground = true;
thread.Start();
}
這樣已經能夠達到效果,但不是微軟案例推薦的寫法。考慮到 ChangeTextBox 方法除了被子執行緒呼叫外,也可能被程式其它部分呼叫。因此,再次修改程式碼如下:
void ChangeTextBox(string str)
{
// InvokeRequired 值判斷當前修改文字框的請求是否有必要交由 UI 執行緒來完成
// 如果為 Ture,說明次訪問控制元件的行為來自子執行緒,則呼叫 Invoke 方法將程式碼執行權交給 UI 執行緒
// 注意,下面實質上是進行了一次方法回撥自身的行為,區別在於再次呼叫自身時,已經是 UI 執行緒在執行了
if (this.InvokeRequired)
{
this.Invoke(new ChangeTextBoxEventHandler(ChangeTextBox), str);
}
else
{
txtNum.Text = str;
}
}
// 增加一個委託
delegate void ChangeTextBoxEventHandler(string str);
// 次迴圈必須在子執行緒上執行,然後將最新值傳遞到 UI 執行緒 文字框才會即時變化
// 如果迴圈寫在 ChangeTextBox 函式中,那麼迴圈真實執行權會交由 UI 執行緒,你只會直接看見結果,看不到過程
void ThreadRun1()
{
for (int i = 0; i < 10000; i++)
{
int num = int.Parse(txtNum.Text);
num++;
ChangeTextBox(num.ToString());
}
}
private void btnThreadB_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ThreadRun1);
thread.IsBackground = true;
thread.Start();
}
與 Invoke 方法相對應的還有 BeginInvoke ()、EndInvoke () 這些非同步方法。無論是同步還是非同步,這些方法總是會通過代理重新回到 UI 執行緒上執行。
這些方法向 UI 執行緒的訊息佇列中放入一個訊息,當 UI 執行緒處理這個訊息時,就會在自己的上下文中執行傳入的方法。換句話說,凡是使用 BeginInvoke 和 Invoke 呼叫的執行緒都是在UI主執行緒中執行的,所以即使這些方法裡涉及到一些靜態變數,也不用考慮加鎖的問題。
ProgressBar 的非同步呼叫
在我們應用程式開發過程中,經常會遇到一些問題,需要使用多執行緒技術來加以解決。
許多種類的應用程式都需要長時間操作,比如:執行一個列印任務,請求一個 Web Service 呼叫等。使用者在這種情況下一般會去轉移做其他事情來等待任務的完成,同時還希望隨時可以監控任務的執行進度。
為什麼在我們切換應用程式後,會發生螢幕假死的現象呢?
這是因為當你切換當前應用程式到後臺再切換回前臺時,系統需要在螢幕上重畫整個使用者介面。但是應用程式正在執行長任務,根本沒有時間處理使用者介面的重畫,問題就會發生。如何解決問題呢?我們需要將長任務放在後臺執行,把使用者介面執行緒解放出來,因此我們需要另外一個執行緒。
如何避免多執行緒的窗體資源訪問的安全問題呢?其實非常簡單,有兩種方法:
- 不管執行緒是否是使用者介面執行緒,對使用者介面資源的訪問統一由委託完成!
- 在每個 Windows Forms 使用者介面類中都有一個 InvokeRequired 屬性,它用來標識當前執行緒是否是來自UI執行緒之外的執行緒。檢查這個屬性的值可以決定是否需要進行非同步呼叫委託。
情況一:
delegate void ShowProgressDelegate(int totalStep, int currentStep);
delegate void RunTaskDelegate(int seconds);
void ShowProgress(int totalStep, int currentStep)
{
progressBar1.Maximum = totalStep;
progressBar1.Value = currentStep;
}
void RunTask(int seconds)
{
ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
// 每 1/4 秒顯示一次進度
for (int i = 0; i < seconds * 4; i++)
{
Thread.Sleep(250);
this.Invoke(showProgress, new object[] { seconds * 4, i + 1 });
}
}
private void button1_Click(object sender, EventArgs e)
{
RunTaskDelegate runTask = new RunTaskDelegate(RunTask);
// 委託非同步呼叫方式
runTask.BeginInvoke(Convert.ToInt32(this.textBox1.Text), null, null);
}
情況二:
delegate void ShowProgressDelegate(int totalStep, int currentStep);
delegate void RunTaskDelegate(int seconds);
void ShowProgress(int totalStep, int currentStep)
{
if (progressBar1.InvokeRequired)
{
ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
this.BeginInvoke(showProgress, new object[] { totalStep, currentStep });
}
else
{
progressBar1.Maximum = totalStep;
progressBar1.Value = currentStep;
}
}
void RunTask(int seconds)
{
// 每 1/4 秒顯示一次進度
for (int i = 0; i < seconds * 4; i++)
{
Thread.Sleep(250);
ShowProgress(seconds * 4, i + 1);
}
}
private void button1_Click(object sender, EventArgs e)
{
RunTaskDelegate runTask = new RunTaskDelegate(RunTask);
// 委託非同步呼叫方式
runTask.BeginInvoke(Convert.ToInt32(this.textBox1.Text), null, null);
}