C#多线程编程介绍——使用thread、threadpool、timer

在system.threading 命名空间提供一些使得能进行多线程编程的类和接口,其中线程的创建有以下三种方法:thread、threadpool、timer。下面我就他们的使用方法逐个作一简单介绍。 
1. thread 
这也许是最复杂的方法,但他提供了对线程的各种灵活控制。首先你必须使用他的构造函数创建一个线程实例,他的参数比较简单,只有一个threadstart 委托: 
public thread(threadstart start);


然后调用start()启动他,当然你能利用他的priority属性来设置或获得他的运行优先级(enum threadpriority: normal、 lowest、 highest、 belownormal、 abovenormal)。

见下例:他首先生成了两个线程实例t1和t2,然后分别设置他们的优先级,接着启动两线程(两线程基本相同,只不过他们输出不相同,t1为“1”,t2为“2”,根据他们各自输出字符个数比可大致看出他们占用cpu时间之比,这也反映出了他们各自的优先级)。

 

  1. static void main(string[] args)   
  2. {   
  3. thread t1 = new thread(new threadstart(thread1));   
  4. thread t2 = new thread(new threadstart(thread2));   
  5.  
  6. t1.priority = threadpriority.belownormal ;   
  7. t2.priority = threadpriority.lowest ;   
  8. t1.start();   
  9. t2.start();   
  10. }   
  11. public static void thread1()   
  12. {   
  13. for (int i = 1; i < 1000; i++)   
  14. {//每运行一个循环就写一个“1”   
  15. dosth();   
  16. console.write("1");   
  17. }   
  18. }   
  19. public static void thread2()   
  20. {   
  21. for (int i = 0; i < 1000; i++)   
  22. {//每运行一个循环就写一个“2”   
  23. dosth();   
  24. console.write("2");   
  25. }   
  26. }   
  27. public static void dosth()   
  28. {//用来模拟复杂运算   
  29. for (int j = 0; j < 10000000; j++)   
  30. {   
  31. int a=15;   
  32. aa = a*a*a*a;   
  33. }   

以上程式运行结果为:

11111111111111111111111111111111111111111121111111111111111111111111111111111111111112

11111111111111111111111111111111111111111121111111111111111111111111111111111111111112

11111111111111111111111111111111111111111121111111111111111111111111111111111111111112

从以上结果我们能看出,t1线程所占用cpu的时间远比t2的多,这是因为t1的优先级比t2的高,若我们把t1和t2的优先级都设为normal,那结果是怎么?他们所占用的cpu时间会相同吗?是的,正如你所料,见下图:

121211221212121212121212121212121212121212121212121212121212121212121

212121212121212121212121212121212121212121212121212121212121212121212

121212121212121212


从上例我们可看出,他的构造类似于win32的工作线程,但更加简单,只需把线程要调用的函数作为委托,然后把委托作为参数构造线程实例即可。当调用start()启动后,便会调用相应的函数,从那函数第一行开始执行。 
接下来我们结合线程的threadstate属性来了解线程的控制

threadstate是个枚举类型,他反映的是线程所处的状态。

当一个thread实例刚创建时,他的threadstate是unstarted;当此线程被调用start()启动之后,他的threadstate是 running; 在此线程启动之后,如果想让他暂停(阻塞),能调用thread.sleep() 方法,他有两个重载方法(sleep(int )、sleep(timespan )),只不过是表示时间量的格式不同而已,当在某线程内调用此函数时,他表示此线程将阻塞一段时间(时间是由传递给 sleep 的毫秒数或timespan决定的,但若参数为0则表示挂起此线程以使其他线程能够执行,指定 infinite 以无限期阻塞线程),此时他的threadstate将变为waitsleepjoin,另外值得注意一点的是sleep()函数被定义为了static?! 这也意味着他不能和某个线程实例结合起来用,也即不存在类似于t1.sleep(10)的调用!正是如此,sleep()函数只能由需“sleep”的线程自己调用,不允许其他线程调用,正如when to sleep是个人私事不能由他人决定。不过当某线程处于waitsleepjoin状态而又不得不唤醒他时,可使用thread.interrupt 方法 ,他将在线程上引发threadinterruptedexception,下面我们先看一个例子(注意sleep的调用方法):

 


  1. static void main(string[] args)   
  2. {   
  3. thread t1 = new thread(new threadstart(thread1));   
  4. t1.start();   
  5. t1.interrupt ();   
  6. e.waitone ();   
  7. t1.interrupt ();   
  8. t1.join();   
  9. console.writeline(“t1 is end”);   
  10. }   
  11. static autoresetevent e = new autoresetevent(false);   
  12. public static void thread1()   
  13. {   
  14. try   
  15. {//从参数可看出将导致休眠   
  16. thread.sleep(timeout.infinite);   
  17. }   
  18. catch(system.threading.threadinterruptedexception e)   
  19. {//中断处理程式   
  20. console.writeline (" 1st interrupt");   
  21. }   
  22. e.set ();   
  23. try   
  24. {// 休眠   
  25. thread.sleep(timeout.infinite );   
  26. }   
  27. catch(system.threading.threadinterruptedexception e)   
  28. {   
  29. console.writeline (" 2nd interrupt");   
  30. }//暂停10秒   
  31. thread.sleep (10000);   

运行结果为: 1st interrupt 
2nd interrupt 
(10s后)t1 is end 
从上例我们能看出thread.interrupt方法能把程式从某个阻塞(waitsleepjoin)状态唤醒进入对应的中断处理程式,然后继续往下执行(他的threadstate也变为running),此函数的使用必须注意以下几点: 
1 .此方法不仅可唤醒由sleep导致的阻塞,而且对一切可导致线程进入waitsleepjoin状态的方法(如wait和join)都有效。如上例所示, 使用时要把导致线程阻塞的方法放入try块内, 并把相应的中断处理程式放入catch块内。 
2 .对某一线程调用interrupt, 如他正处于waitsleepjoin状态, 则进入相应的中断处理程式执行, 若此时他不处于waitsleepjoin状态, 则他后来进入此状态时, 将被即时中断。若在中断前调用几次interrupt, 只有第一次调用有效, 这正是上例我用同步的原因, 这样才能确保第二次调用interrupt在第一个中断后调用,否则的话可能导致第二次调用无效(若他在第一个中断前调用)。你能把同步去掉试试,其结果非常可能是: 1st interrupt


上例还用了另外两个使线程进入waitsleepjoin状态的方法:利用同步对象和thread.join方法。join方法的使用比较简单,他表示在调用此方法的当前线程阻塞直至另一线程(此例中是t1)终止或经过了指定的时间为止(若他还带了时间量参数),当两个条件(若有)任一出现,他即时结束waitsleepjoin状态进入running状态(可根据.join方法的返回值判断为何种条件,为true,则是线程终止;false则是时间到)。 
线程的暂停还可用thread.suspend方法,当某线程处于running状态时对他调用suspend方法,他将进入suspendrequested状态,但他并不会被即时挂起,直到线程到达安全点之后他才能将该线程挂起,此时他将进入suspended状态。如对一个已处于suspended的线程调用则无效,要恢复运行只需调用thread.resume即可。 
线程的销毁,对需销毁的线程调用abort方法,他会在此线程上引发threadabortexception。我们可把线程内的一些代码放入try块内,并把相应处理代码放入相应的catch块内,当线程正执行try块内代码时如被调用abort,他便会跳入相应的catch块内执行,执行完catch快内的代码后他将终止(若catch块内执行了resetabort则不同了:他将取消当前abort请求,继续向下执行。所以如要确保某线程终止的最佳用join,如上例)。 
2. threadpool 
提供一个线程池,该线程池可用于发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。

线程池(threadpool)是一种相对较简单的方法,他适应于一些需要多个线程而又较短任务(如一些常处于阻塞状态的线程) ,他的缺点是对创建的线程不能加以控制,也不能设置其优先级。由于每个进程只有一个线程池,当然每个应用程式域也只有一个线程池(对线),所以你将发现threadpool类的成员函数都为static! 当你首次调用threadpool.queueuserworkitem、threadpool.registerwaitforsingleobject等,便会创建线程池实例。

下面我就线程池当中的两函数作一介绍:

 


  1. public static bool queueuserworkitem( //调用成功则返回true   
  2. waitcallback callback,//要创建的线程调用的委托   
  3. object state //传递给委托的参数   
  4. )//他的另一个重载函数类似,只是委托不带参数而已   
  5. 此函数的作用是把要创建的线程排队到线程池,当线程池的可用线程数不为零时(线程池有创建线程数的限制,缺身值为25),便创建此线程,否则就排队到线程池等到他有可用的线程时才创建。   
  6. public static registeredwaithandle registerwaitforsingleobject(   
  7. waithandle waitobject,// 要注册的 waithandle   
  8. waitortimercallback callback,// 线程调用的委托   
  9. object state,//传递给委托的参数   
  10. int timeout,//超时,单位为毫秒,   
  11. bool executeonlyonce file://是/否只执行一次   
  12. );   
  13. public delegate void waitortimercallback(   
  14. object state,//也即传递给委托的参数   
  15. bool timedout//true表示由于超时调用,反之则因为waitobject   
  16. ); 

此函数的作用是创建一个等待线程,一旦调用此函数便创建此线程,在参数waitobject变为终止状态或所设定的时间timeout到了之前,他都处于“阻塞”状态,值得注意的一点是此“阻塞”和thread的waitsleepjoin状态有非常大的不同:当某thread处于waitsleepjoin状态时cpu会定期的唤醒他以轮询更新状态信息,然后再次进入waitsleepjoin状态,线程的转换可是非常费资源的;而用此函数创建的线程则不同,在触发他运行之前,cpu不会转换到此线程,他既不占用cpu的时间又不浪费线程转换时间,但cpu又怎么知道何时运行他?实际上线程池会生成一些辅助线程用来监视这些触发条件,一旦达到条件便启动相应的线程,当然这些辅助线程本身也占用时间,不过如果你需创建较多的等待线程时,使用线程池的优势就越加明显。见下例:

 


  1. static autoresetevent ev=new autoresetevent(false);   
  2. public static int main(string[] args)   
  3. { threadpool.registerwaitforsingleobject(   
  4. ev,   
  5. new waitortimercallback(waitthreadfunc),   
  6. ,   
  7. ,   
  8. false//表示每次完成等待操作后都重置计时器,直到注销等待   
  9. );   
  10. threadpool.queueuserworkitem (new waitcallback (threadfunc),8);   
  11. thread.sleep (10000);   
  12. return 0;   
  13. }   
  14. public static void threadfunc(object b)   
  15. { console.writeline ("the object is {0}",b);   
  16. for(int i=0;i<2;i++)   
  17. { thread.sleep (1000);   
  18. ev.set();   
  19. }   
  20. }   
  21. public static void waitthreadfunc(object b,bool t)   
  22. { console.writeline ("the object is {0},t is {1}",b,t);   

其运行结果为:

the object is 8

the object is 4,t is false

the object is 4,t is false

the object is 4,t is true

the object is 4,t is true

the object is 4,t is true

从以上结果我们能看出线程threadfunc运行了1次,而waitthreadfunc运行了5次。我们能从waitortimercallback中的bool t参数判断启动此线程的原因:t为false,则表示由于waitobject,否则则是由于超时。另外我们也能通过object b向线程传递一些参数。 
3. timer 
提供以指定的时间间隔执行方法的机制。

使用 TimerCallback 委托指定希望 Timer 执行的方法。 计时器委托在构造计时器时指定,并且不能更改。 此方法不在创建计时器的线程上执行,而是在系统提供的 ThreadPool 线程上执行。

创建计时器时,可以指定在第一次执行方法之前等待的时间量(截止时间)以及此后的执行期间等待的时间量(时间周期)。 可以使用 Change 方法更改这些值或禁用计时器。 
只要在使用 Timer,就必须保留对它的引用。 对于任何托管对象,如果没有对 Timer 的引用,计时器会被垃圾回收。 即使 Timer 仍处在活动状态,也会被回收。 
当不再需要计时器时,请使用 Dispose 方法释放计时器持有的资源。 如果希望在计时器被释放时接收到信号,请使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重载。 计时器已被释放后,WaitHandle 便终止。

由计时器执行的回调方法应该是可重入的,因为它是在 ThreadPool 线程上调用的。 在以下两种情况中,此回调可以同时在两个线程池线程上执行:一是计时器间隔小于执行此回调所需的时间;二是所有线程池线程都在使用,此回调被多次排队。

System.Threading.Timer 是一个简单的轻量计时器,它使用回调方法并由线程池线程提供服务。 不建议将其用于 Windows 窗体,因为其回调不在用户界面线程上进行。 System.Windows.Forms.Timer 是用于 Windows 窗体的更佳选择。 要获取基于服务器的计时器功能,可以考虑使用 System.Timers.Timer,它可以引发事件并具有其他功能。

这和win32中的settimer方法类似。他的构造为:

public timer( 
timercallback callback,
//所需调用的方法 
object state,//传递给callback的参数 
int duetime,//多久后开始调用callback 
int period//调用此方法的时间间隔 
);

// 如果 duetime 为0,则 callback 即时执行他的首次调用。如果 duetime 为 infinite,则 callback 不调用他的方法。计时器被禁用,但使用 change 方法能重新启用他。如果 period 为0或 infinite,并且 duetime 不为 infinite,则 callback 调用他的方法一次。计时器的定期行为被禁用,但使用 change 方法能重新启用他。如果 period 为零 (0) 或 infinite,并且 duetime 不为 infinite,则 callback 调用他的方法一次。计时器的定期行为被禁用,但使用 change 方法能重新启用他。 
在创建计时器之后若想改动他的period和duetime,我们能通过调用timer的change方法来改动: 
[c#] 
public bool change( 
int duetime, 
int period 
);//显然所改动的两个参数对应于timer中的两参数 
见下例:

 


  1. public static int main(string[] args)   
  2.  
  3. { console.writeline ("period is 1000");   
  4. timer tm=new timer (new timercallback (timercall),3,1000,1000);   
  5. thread.sleep (2000);   
  6. console.writeline ("period is 500");   
  7. tm.change (0,800);   
  8. thread.sleep (3000);   
  9. return 0;   
  10. }   
  11.  
  12. public static void timercall(object b)   
  13. {   
  14. console.writeline ("timercallback; b is {0}",b);   

其运行结果为:

period is 1000

timercallback;b is 3

timercallback;b is 3

period is 500

timercallback;b is 3

timercallback;b is 3

timercallback;b is 3

timercallback;b is 3 
总结 
从以上的简单介绍,我们能看出他们各自使用的场合:thread适用于那些需对线程进行复杂控制的场合;threadpool适应于一些需要多个线程而又较短任务(如一些常处于阻塞状态的线程);timer则适用于那些需周期性调用的方法。只要我们了解了他们的使用特点,我们就能非常好的选择合适的方法。



本文转自linzheng 51CTO博客,原文链接:http://blog.51cto.com/linzheng/1085602

上一篇:【云周刊】第170期:技术人生-看90后如何逆袭,从实习生成长为阿里云分布式NoSQL领域专家


下一篇:Java基础篇(02):特殊的String类,和相关扩展API