1. 程式人生 > >C# 多執行緒詳解 Part.02(UI 執行緒和子執行緒的互動、ProgressBar 的非同步呼叫)

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();
    }
}

image 

       微軟在子執行緒修改 UI 執行緒的控制元件值時給出的安全限制方案為:在 VS2005 或者更高版本中,只要不是在控制元件的建立執行緒(一般就是指UI主執行緒)上訪問控制元件的屬性就會丟擲這個錯誤,解決方法就是利用控制元件提供的 Invoke 和 BeginInvoke 把呼叫封送回 UI 執行緒,也就是讓控制元件屬性修改在UI執行緒上執行;或者 禁用此安全限制。

       解決方案一:解除該控制元件上對錯誤執行緒呼叫的檢查(謹慎使用)。

public
Form2()
{
    InitializeComponent();
    // 解除 TextBox 對錯誤執行緒呼叫的檢查
    // 如果要捕獲了對錯誤執行緒的呼叫,則為 true(預設值);否則為 false
    // 對控制元件許可權可以開放的更大 例如 Control、Form 等
    TextBox.CheckForIllegalCrossThreadCalls = false; 
}

       解決方案二:

void ChangeTextBox(string str)
{
    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++;
        // 使用 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 呼叫等。使用者在這種情況下一般會去轉移做其他事情來等待任務的完成,同時還希望隨時可以監控任務的執行進度。

       為什麼在我們切換應用程式後,會發生螢幕假死的現象呢?

       這是因為當你切換當前應用程式到後臺再切換回前臺時,系統需要在螢幕上重畫整個使用者介面。但是應用程式正在執行長任務,根本沒有時間處理使用者介面的重畫,問題就會發生。如何解決問題呢?我們需要將長任務放在後臺執行,把使用者介面執行緒解放出來,因此我們需要另外一個執行緒。

       如何避免多執行緒的窗體資源訪問的安全問題呢?其實非常簡單,有兩種方法:

  1. 不管執行緒是否是使用者介面執行緒,對使用者介面資源的訪問統一由委託完成!
  2. 在每個 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);
}

image

情況二:

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);
}