博客园  :: 首页  :: 新随笔  :: 联系 :: 管理

C#线程相关问题总结:基本操作及UI控件交互

Posted on 2009-11-23 15:32  codingsilence  阅读(301)  评论(0编辑  收藏  举报

 

    C#线程相关问题有很多,有多线程的,有单线程的。本文主要讨论C#单线程操作以及与UI控件互交方面的一些注意事项。

     

    C#线程在.NET中是一项十分值得探讨的对象。C#是一门支持多线程的语言,因此线程的使用也是比较常见的。由于线程的知识在Win32编程的时候已经说得过多,所以在.Net中很少介绍这部分(可能.Net不觉得这部分是它所特有的)。

    那么线程相关的问题大致有如下四类(这篇文章只讨论单线程、单线程与UI线程这两方面的问题)。

    问题一,线程的基本操作,例如:暂停、继续、停止等;

    问题二,如何向线程传递参数或者从中得到其返回值;

    问题三,如何使线程所占用的CPU不要老是百分之百;

    最后一个,也是问题最多的,就是如何在子线程来控制UI中的控件,换句话说,就是在线程中控制窗体某些控件的显示。

    对于问题一,我不建议使用Thread类提供的Suspend、Resume以及Abort这三个方法,前两个有问题,好像在VS05已经屏蔽这两个方法;对于Abort来说,除了资源没有得到及时释放外,有时候会出现异常。如何做呢,通过设置开关变量来完成。

    对于问题二,我不建议使用静态成员来完成,仅仅为了线程而破坏类的封装有些得不偿失。那如何做呢,通过创建单独的线程类来完成。

    对于问题三来说,造成这个原因是由于线程中进行不间断的循环操作,从而使CPU完全被子线程占有。那么处理此类问题,其实很简单,在适当的位置调用Thread.Sleep(20)来释放所占有CPU资源,不要小看这20毫秒的睡眠,它的作用可是巨大的,可以使其他线程得到CPU资源,从而使你的CPU使用效率降下来。

    看完前面的三个问题的解释,对于如何做似乎没有给出一个明确的答案,为了更好地说明如何解决这三个问题,我用一个比较完整的例子展现给大家,代码如下。

    1. //--------------------------- Sub-thread class ---------------------------------------  
    2. //------------------------------------------------------------------------------------  
    3. //---File:          clsSubThread  
    4. //---Description:   The sub-thread template class file  
    5. //---Author:        Knight  
    6. //---Date:          Aug.21, 2006  
    7. //------------------------------------------------------------------------------------  
    8. //---------------------------{Sub-thread class}---------------------------------------  
    9. namespace ThreadTemplate  
    10. {  
    11.     using System;  
    12.     using System.Threading;  
    13.     using System.IO;  
    14.     /// < summary>  
    15.     /// Summary description for clsSubThread.  
    16.     /// < /summary>  
    17.     public class clsSubThread:IDisposable  
    18.     {  
    19.         private Thread thdSubThread = null;  
    20.         private Mutex mUnique = new Mutex();  
    21.         private bool blnIsStopped;  
    22.         private bool blnSuspended;  
    23.         private bool blnStarted;  
    24.         private int nStartNum;  
    25.         public bool IsStopped  
    26.         {  
    27.             getreturn blnIsStopped; }  
    28.         }  
    29.         public bool IsSuspended  
    30.         {  
    31.             getreturn blnSuspended; }  
    32.         }  
    33.         public int ReturnValue  
    34.         {  
    35.             getreturn nStartNum;}  
    36.         }  
    37.      
    38.         public clsSubThread( int StartNum )  
    39.         {  
    40.             //  
    41.             // TODO: Add constructor logic here  
    42.             //  
    43.             blnIsStopped = true;  
    44.             blnSuspended = false;  
    45.             blnStarted = false;  
    46.              
    47.             nStartNum = StartNum;  
    48.         }  
    49.         /// < summary>  
    50.         /// Start sub-thread  
    51.         /// < /summary>  
    52.         public void Start()  
    53.         {  
    54.             if( !blnStarted )  
    55.             {  
    56.                 thdSubThread = new Thread( new ThreadStart( SubThread ) );  
    57.                 blnIsStopped = false;  
    58.                 blnStarted = true;  
    59.                 thdSubThread.Start();  
    60.             }  
    61.         }  
    62.         /// < summary>  
    63.         /// Thread entry function  
    64.         /// < /summary>  
    65.         private void SubThread()  
    66.         {  
    67.             do 
    68.             {  
    69.                 // Wait for resume-command if got suspend-command here   
    70.                 mUnique.WaitOne();  
    71.                 mUnique.ReleaseMutex();  
    72.                 nStartNum++;  
    73.              
    74.                 Thread.Sleep(1000); // Release CPU here  
    75.             }while( blnIsStopped == false );  
    76.         }  
    77.         /// < summary>  
    78.         /// Suspend sub-thread  
    79.         /// < /summary>  
    80.         public void Suspend()  
    81.         {  
    82.             if( blnStarted && !blnSuspended )  
    83.             {  
    84.                 blnSuspended = true;  
    85.                 mUnique.WaitOne();  
    86.             }  
    87.         }  
    88.      
    89.         /// < summary>  
    90.         /// Resume sub-thread  
    91.         /// < /summary>  
    92.         public void Resume()  
    93.         {  
    94.             if( blnStarted && blnSuspended )  
    95.             {  
    96.                 blnSuspended = false;  
    97.                 mUnique.ReleaseMutex();  
    98.             }  
    99.         }  
    100.         /// < summary>  
    101.         /// Stop sub-thread  
    102.         /// < /summary>  
    103.         public void Stop()  
    104.         {  
    105.             if( blnStarted )  
    106.             {  
    107.                 if( blnSuspended )  
    108.                     Resume();  
    109.                 blnStarted = false;  
    110.                 blnIsStopped = true;  
    111.                 thdSubThread.Join();  
    112.             }  
    113.         }  
    114.         #region IDisposable Members  
    115.         /// < summary>  
    116.         /// Class resources dispose here  
    117.         /// < /summary>  
    118.         public void Dispose()  
    119.         {  
    120.             // TODO:  Add clsSubThread.Dispose implementation  
    121.             Stop();//Stop thread first  
    122.             GC.SuppressFinalize( this );  
    123.         }  
    124.         #endregion  
    125.     }  
    126. }  
    127.  

    那么对于调用呢,就非常简单了,如下:

    1. // Create new sub-thread object with parameters  
    2. clsSubThread mySubThread = new clsSubThread( 5 );  
    3. mySubThread.Start();//Start thread  
    4.  
    5. Thread.Sleep( 2000 );  
    6. mySubThread.Suspend();//Suspend thread   
    7. Thread.Sleep( 2000 );  
    8. mySubThread.Resume();//Resume thread   
    9. Thread.Sleep( 2000 );  
    10. mySubThread.Stop();//Stop thread   
    11. //Get thread's return value  
    12. Debug.WriteLine( mySubThread.ReturnValue );  
    13. //Release sub-thread object  
    14. mySubThread.Dispose();  

    在回过头来看看前面所说的三个问题。

    C#线程相关问题回顾:问题一

    对于问题一来说,首先需要局部成员的支持,那么

    1. private Mutex mUnique = new Mutex();  
    2. private bool blnIsStopped;  
    3. private bool blnSuspended;  
    4. private bool blnStarted;  

    光看成员名称,估计大家都已经猜出其代表的意思。接下来需要修改线程入口函数,要是这些开关变量能发挥作用,那么看看SubThread这个函数。

    1. /// < summary>  
    2. /// Thread entry function  
    3. /// < /summary>  
    4. private void SubThread()  
    5. {  
    6.     do 
    7.     {  
    8.         // Wait for resume-command if got suspend-command here    
    9.         mUnique.WaitOne();  
    10.         mUnique.ReleaseMutex();  
    11.         nStartNum++;  
    12.       
    13.         Thread.Sleep(1000);  
    14.     }while( blnIsStopped == false );  
    15. }  

    函数比较简单,不到十句,可能对于“blnIsStopped == false”这个判断来说,大家还比较好理解,这是一个普通的判断,如果当前Stop开关打开了,就停止循环;否则一直循环。

    大家比较迷惑的可能是如下这两句:

    1. mUnique.WaitOne();  
    2. mUnique.ReleaseMutex();  

    这两句的目的是为了使线程在Suspend操作的时候能发挥效果,为了解释这两句,需要结合Suspend和Resume这两个方法,它俩的代码如下。

    1. /// < summary>  
    2. /// Suspend sub-thread  
    3. /// < /summary>  
    4. public void Suspend()  
    5. {  
    6.     if( blnStarted && !blnSuspended )  
    7.     {  
    8.         blnSuspended = true;  
    9.         mUnique.WaitOne();  
    10.     }  
    11. }  
    12.  
    13. /// < summary>  
    14. /// Resume sub-thread  
    15. /// < /summary>  
    16. public void Resume()  
    17. {  
    18.     if( blnStarted && blnSuspended )  
    19.     {  
    20.         blnSuspended = false;  
    21.         mUnique.ReleaseMutex();  
    22.     }  
    23. }  

    为了更好地说明,还需要先简单说说Mutex类型。对于此类型对象,当调用对象的WaitOne之后,如果此时没有其他线程对它使用的时候,就立刻获得信号量,继续执行代码;当再调用ReleaseMutex之前,如果再调用对象的WaitOne方法,就会一直等待,直到获得信号量的调用ReleaseMutex来进行释放。这就好比卫生间的使用,如果没有人使用则可以直接使用,否则只有等待。

    明白了这一点后,再来解释这两句所能出现的现象。

    1. mUnique.WaitOne();  
    2. mUnique.ReleaseMutex();  

    当在线程函数中,执行到“mUnique.WaitOne();”这一句的时候,如果此时外界没有发送Suspend消息,也就是信号量没有被占用,那么这一句可以立刻返回。那么为什么要紧接着释放呢,因为不能总占着信号量,立即释放信号量是避免在发送Suspend命令的时候出现等待;如果此时外界已经发送了Suspend消息,也就是说信号量已经被占用,此时“mUnique.WaitOne();”不能立刻返回,需要等到信号量被释放才能继续进行,也就是需要调用Resume的时候,“mUnique.WaitOne();”才能获得信号量进行继续执行。这样才能达到真正意义上的Suspend和Resume。

    至于线程的Start和Stop来说,相对比较简单,这里我就不多说了。

    C#线程相关问题回顾:问题二、三

    现在再来分析一下问题二,其实例子比较明显,是通过构造函数和属性来完成参数和返回值,这一点我也不多说了。如果线程参数比较多的话,可以考虑属性来完成,类似于返回值。

    问题三,我就更不用多说了。有人说了,如果子线程中的循环不能睡眠怎么办,因为睡眠的话,有时会造成数据丢失,这方面的可以借鉴前面Suspend的做法,如果更复杂,则牵扯到多线程的同步问题,这部分我会稍后单独写一篇文章。

    前三个问题解决了,该说说最常见的问题,如何在子线程中控制窗体控件。这也是写线程方面程序经常遇到的。

    首先说说,为什么不能直接在子线程中操纵UI呢。原因在于子线程和UI线程属于不同的上下文,换句比较通俗的话说,就好比两个人在不同的房间里一样,那么要你直接操作另一个房间里的东西,恐怕不行罢,那么对于子线程来说也一样,不能直接操作UI线程中的对象。

    那么如何在子线程中操纵UI线程中的对象呢,.Net提供了Invoke和BeginInvoke这两种方法。简单地说,就是子线程发消息让UI线程来完成相应的操作。

    这两个方法有什么区别,这在我以前的文章已经说过了,Invoke需要等到所调函数的返回,而BeginInvoke则不需要。

    用这两个方法需要注意的,有如下三点:

    第一个是由于Invoke和BeginInvoke属于Control类型的成员方法,因此调用的时候,需要得到Control类型的对象才能触发,也就是说你要触发窗体做什么操作或者窗体上某个控件做什么操作,需要把窗体对象或者控件对象传递到线程中。

    第二个,对于Invoke和BeginInvoke接受的参数属于一个delegate类型,我在以前的文章中使用的是MethodInvoker,这是.Net自带的一个delegate类型,而并不意味着在使用Invoke或者BeginInvoke的时候只能用它。参看我给的第二篇文章(《如何弹出一个模式窗口来显示进度条》),会有很多不同的delegate定义。

    最后一个,使用Invoke和BeginInvoke有个需要注意的,就是当子线程在Form_Load开启的时候,会遇到异常,这是因为触发Invoke的对象还没有完全初始化完毕。处理此类问题,在开启线程之前显式的调用“this.Show();”,来使窗体显示在线程开启之前。如果此时只是开启线程来初始化显示数据,那我建议你不要使用子线程,用Splash窗体的效果可能更好。这方面可以参看如下的例子。

    http://www.syncfusion.com/FAQ/WindowsForms/FAQ_c95c.aspx#q621q

    线程的四个相关问题已经说完了,这篇文章只说了单线程,以及单线程与UI线程交互的问题。其中涉及到的方法不一定是唯一的,因为.Net还提供了其他类来扶助线程操作,这里就不一一罗列。