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線程上執行;或者 禁用此安全限制。多線程

       解決方案一:解除該控件上對錯誤線程調用的檢查(謹慎使用)。app

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 線程的消息隊列中放入一個消息,當 UI 線程處理這個消息時,就會在本身的上下文中執行傳入的方法。換句話說,凡是使用 BeginInvoke 和 Invoke 調用的線程都是在UI主線程中執行的,因此即便這些方法裏涉及到一些靜態變量,也不用考慮加鎖的問題。this

 

 

ProgressBar 的異步調用

       在咱們應用程序開發過程當中,常常會遇到一些問題,須要使用多線程技術來加以解決。spa

       許多種類的應用程序都須要長時間操做,好比:執行一個打印任務,請求一個 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);
}
相關文章
相關標籤/搜索