UML软件工程组织

C#多线程应用探讨
作者: 王志喜 王润云
Thursday, September 18 2003 4:15 PM     
2000年6月,Microsoft发布了一种新的程序设计语言——C#。C#是一种现代的,面向对象的语言,它使开发人员能够在Microsoft .NET框架上快速建立广泛的应用。C#支持建立自由线程(free-threaded)的应用,多个线程可以访问同一套共享数据。
实例程序说明

本文的实例程序包括一个列表框、三个按钮。程序使用一个新的线程来运行一个后台处理,结果在列表框中显示。按钮button1启动一个计算平方的线程。按钮button2停止后台处理线程。按钮button3退出程序。程序运行情况如图1所示。

使用线程

首先创建运行在新线程上的后台任务。表1所示的代码执行一个相当长的运行处理----一个无限循环。

表1、后台处理程序
private void BackgroundProcess()
{
       int i= 1;
       while(true)
       {
              //  向列表框增加一个项目
              listBox1.Items.Add("Iterations: " + i.ToString ());
              i ++;
              Thread.Sleep(2000);  //  指定线程休眠的时间
       }
}

这段代码无限循环,每次执行时在列表框中加入一个项目。

在规定好一个工作的处理代码以后,就需要将这段代码分配给一个线程,并且启动它。为此需要使用线程对象(Thread object),它是.NET架构类中System.Threading命名空间的一部分。在实例化一个新的线程类时,需要把在线程类构造器中执行的代码块的一个引用传送给该实例。表2所示的代码创建一个新的线程对象,并且将BackgroundProcess的一个引用传送给该对象。

表2、线程的使用
Thread t1,t2;  //  说明为窗体类成员
t1 = new Thread(new ThreadStart(BackgroundProcess));
t1.Start();  //  以上2行放置在窗体的load事件中

ThreadStart表示在线程上执行的方法,这里是一个到BackgroundProcess方法的委派对象。在C#中,一个委派是一个类型安全、面向对象的函数指针。在实例化该线程后,可以通过调用线程的Start()方法来开始执行代码。

控制线程

在线程启动以后,可以通过调用线程对象的方法来控制线程的状态。可以通过调用Thread.Sleep方法来暂停一个线程的执行,这个方法可以接收一个整型值,用来决定线程休眠的时间。对于本文的实例程序,为了让列表项目增加的速度变慢,在其中放入了一个Sleep方法的调用。

可以通过调用Thread.Sleep(System.Threading.Timeout.Infinite)来让线程进入休眠状态,但是,这个调用的休眠时间是不确定的。要中断这个休眠,可以调用Thread.Interrupt方法。

通过调用Thread.Suspend方法可以挂起线程。挂起可以暂停一个线程,直到另一个线程调用Thread.Resume为止。休眠和挂起的区别是,挂起并不立刻让线程进入一个等待的状态,线程并不会挂起,直到.NET runtime认为现在已经是一个安全的地方来挂起它了,而休眠则会立刻让线程进入一个等待的状态。

表3、停止线程的执行
private void button2_Click
(object sender, System.EventArgs e)
{     t1.Abort();      }

Thread.Abort方法可以停止一个线程的执行。本文的实例程序通过加入一个按钮button2来停止后台处理,在事件处理程序中调用了Thread.Abort方法,如表3所示。

这就是多线程的强大之处。用户界面的响应很快,因为用户界面运行在一个单独的线程中,而后台的处理运行在另外一个线程中。在用户按下按钮button2时,就会马上得到响应,并且停止后台处理。

通过多线程程序传送数据

在实际工作中,还需要使用到多线程的许多复杂特性。其中一个问题就是如何将程序的数据由线程类的构造器传入或者传出。对于放到另外一个线程中的过程,既不能传参数给它,也不能由它返回值,因为传入到线程构造器的过程是不能拥有任何参数或者返回值的。为了解决这个问题,可以将过程封装到一个类中,这样,方法的参数就可使用类中的字段。

本文给出了一个简单的例子,计算一个数的平方。为了在一个新的线程中使用这个过程,将它封装到一个类中,如表4所示。

使用表5所示的代码在一个新的线程上启动CalcSquare过程。

表4、计算一个数的平方   表5、在一个新的线程上启动CalcSquare过程
public class SquareClass
{
      public  double Value;
      public double Square;
      public void CalcSquare()
      {
             Square = Value * Value;
      }
}
  private void button1_Click(object sender, System.EventArgs e)
{
      SquareClass oSquare =new SquareClass();
      t2 = new Thread(new ThreadStart(oSquare.CalcSquare));
      oSquare.Value = 30;
      t2.Start();
}

 

在上述例子中,线程启动后,并没有检查类中的square值,因为即使调用了线程的start方法,也不能确保其中的方法马上执行完。要从另一个线程中得到需要的值,有几种方法,其中一种方法就是在线程完成的时候触发一个事件。表6所示的代码为SquareClass加入了事件声明。

表6、为SquareClass加入事件声明
public delegate void EventHandler(double sq);  //  说明委派类型
public class SquareClass
{    
      public  double Value;
      public double Square;
      public event EventHandler ThreadComplete;  //  说明事件对象
      public void CalcSquare()
      {
             Square = Value * Value;
             //  指定事件处理程序
             ThreadComplete+=new EventHandler(SquareEventHandler);
             if( ThreadComplete!=null)ThreadComplete(Square);  //  触发事件
      }
      public static void SquareEventHandler(double  Square )  //  定义事件处理程序
      {     MessageBox.Show(Square.ToString ());      }
}

 

对于这种方法,要注意的是事件处理程序SquareEventHandler运行在产生该事件的线程t2中,而不是运行在窗体执行的线程中。

同步线程

在线程的同步方面,C#提供了几种方法。在上述计算平方的例子中,需要与执行计算的线程同步,以便等待它执行完并且得到结果。另一个例子是,如果在其它线程中排序一个数组,那么在使用该数组前,必须等待该处理完成。为了实现同步,C#提供了lock声明和Thread.Join方法。

lock声明

表7、使用lock声明
public void CalcSquare1()
{
      lock( typeof(SquareClass))
      {
             Square = Value * Value;
      }
}

lock可以得到一个对象引用的唯一锁,使用时只要将该对象传送给lock就行了。通过这个唯一锁,可以确保多个线程不会访问共享的数据或者在多个线程上执行的代码。要得到一个锁,可以使用与每个类关联的System.Type对象。System.Type对象可以通过使用typeof运算得到,如表7所示。

Thread.Join方法

表8、使用Thread.Join方法
private void button1_Click(object sender, System.EventArgs e)
{
      SquareClass oSquare =new SquareClass();
      t2 = new Thread(new ThreadStart(oSquare.CalcSquare));
      oSquare.Value = 30;
      t2.Start();
      if( t2.Join (500) )
      {
             MessageBox.Show(oSquare.Square.ToString ());
      }
}

Thread.Join方法可以等待一个特定的时间,直到一个线程完成。如果该线程在指定的时间内完成了,Thread.Join将返回True,否则它返回False。在上述平方的例子中,如果不想使用触发事件的方法,可以调用Thread.Join的方法来确定计算是否完成了。代码如表8所示。

结论

本文通过一个实例程序说明了C#中线程的使用和控制方法,探讨了如何通过多线程程序传送数据和线程的同步问题。根据本文的分析可知,在C#中,使用线程是很简单的。C#支持建立自由线程的应用,提高了资源的利用率,程序的响应速度也得到了改善。当然也带来了数据传送和线程同步等问题。

参考文献

[1]      袁鹏飞,C#和.NET架构[M],人民邮电出版社,北京,2002年4月第1版,123-125

[2]      麦中凡等,C#编程语言[M],北京航空航天大学出版社,北京,2001年8月第1版,286-291

[3]      微软公司、东方人华,C#语言参考手册[M],清华大学出版社,北京,2001年7月第1版,271-274

[4]      李太君、张树亮,利用Visual Basic.Net开发多线程应用程序[J],现代计算机,2002年第11期

[5]      邱艳宇等,VB.NET多线程在数据采集与处理系统中的应用[J],微型机与应用,2002年第08期


湖南省自然科学基金(编号:02JJY4045)资助项目

作者简介:王志喜,男,1970年生,硕士,湖南科技大学计算机学院副教授,主要研究方向为:面向对象技术,软件工程等。王润云,1960年生,女,湖南科技大学计算机学院副教授,主要研究方向为:程序设计语言,面向对象技术,计算机辅助设计等。


 


版权所有:UML软件工程组织