线程类的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); }
下一篇介绍如何让线程退出和等待,并介绍为什么不对所有方法都使用线程…