線程類的Suspend() 和 Resume() 方法可以用來掛起/恢復線程。Suspend()方法將會立即掛起當前線程直到另外一個線程把它喚醒。當我們調用Suspend()方法時,線程將會進入SuspendRequested 或者 Suspended 狀態。
我們來看一個例子。我們創建一個新的C#應用程序並在一個新線程中生成素數。這個應用程序有掛起以及恢復素數生成線程的選項。為了方便操作和演示,我們創建一個新的C# 窗體應用程序, PrimeNumbers:
程序界面上有一個列表和 三個控制按鈕。列表用來顯示素數,三個控制按鈕用來啟動、掛起以及恢復線程。初始化時我們會將掛起和恢復按鈕禁用,由於這些按鈕在程序啟動前無法使用,所以咱們來看看后台代碼是如何實現的?我們聲明了一個將要生成素數的線程對象(類成員)。
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:oDaniel Dong * Blog:o www.cnblogs.com/danielWise * Email:o guofoo@163.com * */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Threading; namespace Chapter02 { public partial class Form1 : Form { //private thread viriable private Thread primeNumberThread; public Form1() { InitializeComponent(); } private void cmdStart_Click(object sender, EventArgs e) { //Let's create a new thread. primeNumberThread = new Thread( new ThreadStart(GeneratePrimeNumbers)); primeNumberThread.Name = "Prime Numbers Example"; primeNumberThread.Priority = ThreadPriority.BelowNormal; cmdPause.Enabled = true; cmdStart.Enabled = false; primeNumberThread.Start(); }
開始按鈕創建了一個線程並委托GeneratePrimeNumbers()方法,然后將線程命名為“Prime Number Example”。接下來啟用暫停按鈕並禁用開始按鈕。下一步它使用線程類的開始方法啟動生成素數的線程。
雙擊暫停按鈕並輸入以下代碼:
private void cmdPause_Click(object sender, EventArgs e) { try { //If current state of thread is Running, //then pause the thread if (primeNumberThread.ThreadState == ThreadState.Running) { //Pause the thread primeNumberThread.Suspend(); //Disable the Pause button cmdPause.Enabled = false; //Enable the resume button cmdResume.Enabled = true; } } catch (ThreadStateException ex) { MessageBox.Show(ex.ToString(), "Exception", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1); } }
暫停按鈕檢查線程的狀態是否為Running. 如果是的話,它會通過調用線程對象的掛起方法暫停線程執行。然后啟用恢復按鈕並禁用暫停按鈕。由於掛起方法會導致ThreadStateException異常,我們把代碼包裝到一個try…catch 語句中。
雙擊恢復按鈕並加入以下代碼:
private void cmdResume_Click(object sender, EventArgs e) { if (primeNumberThread.ThreadState == ThreadState.SuspendRequested || primeNumberThread.ThreadState == ThreadState.Suspended) { try { primeNumberThread.Resume(); cmdResume.Enabled = false; cmdPause.Enabled = true; } catch (ThreadStateException ex) { MessageBox.Show(ex.ToString(), "Exception", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1); } } }
恢復按鈕在恢復線程前會檢查線程的狀態是否為Suspended 或者SuspendRequested. 如果狀態是二者之一那么它會恢復線程並禁用恢復按鈕,啟用暫停按鈕。
額,到目前為止我們的商業邏輯已經完成。我們看一下生成素數的代碼。由於我們的主要目的是使用多線程而不是如何生成素數,所以不深入介紹【查看評論1】。GeneratePrimeNumbers()方法從3開始生成256個素數。當方法找到一個素數時,它將向一個數組和列表中同時添加值。第一個素數是2,我們將2直接加到列表中。最后,這個方法將會啟用開始按鈕並禁用掛起按鈕。
public void GeneratePrimeNumbers() { long lngCounter = 2; long lngNumber = 3; long lngDivideByCounter; bool blnIsPrime; long[] PrimeArray = new long[256]; //We know that the first prime is 2. //Therefore, let's add it to the list and start from 3. PrimeArray[1] = 2; lstPrime.Items.Add(2); while (lngCounter < 256) { blnIsPrime = true; //Try dividing this number by any already found prime //Which is smaller then the root of this number. for (lngDivideByCounter = 1; PrimeArray[lngDivideByCounter] * PrimeArray[lngDivideByCounter] <= lngNumber; lngDivideByCounter++) { if (lngNumber % PrimeArray[lngDivideByCounter] == 0) { //This is not a prime number blnIsPrime = false; //Exit the loop break; } } //If this is a prime number then display it if (blnIsPrime) { //Guess we found a new prime. PrimeArray[lngCounter] = lngNumber; //Increase prime found count. lngCounter++; lstPrime.Items.Add(lngNumber); //Sleep 100 milliseconds. //This will simulate the time lag and we'll get time //to pause and resume the thread. Thread.Sleep(100); } lngNumber += 2; } cmdStart.Enabled = true; cmdPause.Enabled = false; }
現在所有的准備工作都做好了,讓我們運行代碼來看看我們的程序長什么樣?
額,看起來不錯嗎?No! 我們的代碼中有一個很大的問題。當GeneratePrimeNumbers()方法找到一個素數時,它會將素數添加回列表控件。如果在一個同步執行情況下,比如素數生成代碼和用戶界面都在同一個線程中的話,那么以上代碼看起來沒有什么問題。但是在我們的例子中,用戶界面部分與GeneratePrimeNumbers()方法運行在兩個不同的線程中。因此,當我們在程序中跨線程寫數據時將會導致一些不可預期的行為。
處理這種問題最好的方法是使用委托。我們可以定義一個委托同時我們可以使用委托來通知用戶界面線程來自己更新列表控件。在這種方式下,我們不用跨越線程邊界同時應用程序穩定性不會遭到破壞。
現在來看一下如何改進這個操作。讓我們添加一個公共委托UpdateData:
public delegate void UpdateData(string returnVal);
接下來稍微修改一下GeneratePrimeNumbers()方法以便於調用委托。我們已經添加了一個新的字符串數組並賦初始值2。下面我們把類型UpdateData 定義為委托並把UpdateUI方法地址傳給它。最后我們通過委托和字符串數組調用這個方法並通知用戶接口自己進行更新。
public void GeneratePrimeNumbers() { long lngCounter = 2; long lngNumber = 3; long lngDivideByCounter; bool blnIsPrime; long[] PrimeArray = new long[256]; string[] args = new string[] { "2" }; UpdateData UIDel = new UpdateData(UpdateUI); UpdateButtonControl UIControl = new UpdateButtonControl(UpdateButtonState); //We know that the first prime is 2. //Therefore, let's add it to the list and start from 3. PrimeArray[1] = 2; //lstPrime.Items.Add(2); this.Invoke(UIDel, args); while (lngCounter < 256) { blnIsPrime = true; //Try dividing this number by any already found prime //Which is smaller then the root of this number. for (lngDivideByCounter = 1; PrimeArray[lngDivideByCounter] * PrimeArray[lngDivideByCounter] <= lngNumber; lngDivideByCounter++) { if (lngNumber % PrimeArray[lngDivideByCounter] == 0) { //This is not a prime number blnIsPrime = false; //Exit the loop break; } } //If this is a prime number then display it if (blnIsPrime) { //Guess we found a new prime. PrimeArray[lngCounter] = lngNumber; //Increase prime found count. lngCounter++; //lstPrime.Items.Add(lngNumber); args[0] = lngNumber.ToString(); this.Invoke(UIDel, args); //Sleep 100 milliseconds. //This will simulate the time lag and we'll get time //to pause and resume the thread. Thread.Sleep(100); } lngNumber += 2; } this.Invoke(UIControl, new string[] { "cmdStart", "true" }); this.Invoke(UIControl, new string[] { "cmdPause", "false" }); }
UpdateUI()方法簡單地將需要添加到列表中的值作為它的參數並添加到列表中去。由於UpdateUI方法運行在UI線程中,所以沒有跨線程邊界操作同時我們程序的穩定性也不會被破壞:
void UpdateUI(string result) { lstPrime.Items.Add(result); }
下一篇介紹如何讓線程退出和等待,並介紹為什么不對所有方法都使用線程…