c#线程Thread示例
程序员文章站
2024-02-12 09:31:31
c#是一门支持多线程的语言,因此线程的使用也是比较常见的。由于线程的知识在win32编程的时候已经说得过多,所以在.net中很少介绍这部分(可能.net不觉得这部分是它所特...
c#是一门支持多线程的语言,因此线程的使用也是比较常见的。由于线程的知识在win32编程的时候已经说得过多,所以在.net中很少介绍这部分(可能.net不觉得这部分是它所特有的)。
那么线程相关的问题大致有如下四类(这篇文章只讨论单线程、单线程与ui线程这两方面的问题)。
问题一,线程的基本操作,例如:暂停、继续、停止等;
问题二,如何向线程传递参数或者从中得到其返回值;
问题三,如何使线程所占用的cpu不要老是百分之百;
最后一个,也是问题最多的,就是如何在子线程来控制ui中的控件,换句话说,就是在线程中控制窗体某些控件的显示。
对于问题一,我不建议使用thread类提供的suspend、resume以及abort这三个方法,前两个有问题,好像在vs05已经屏蔽这两个方法;对于abort来说,除了资源没有得到及时释放外,有时候会出现异常。如何做呢,通过设置开关变量来完成。
对于问题二,我不建议使用静态成员来完成,仅仅为了线程而破坏类的封装有些得不偿失。那如何做呢,通过创建单独的线程类来完成。
对于问题三来说,造成这个原因是由于线程中进行不间断的循环操作,从而使cpu完全被子线程占有。那么处理此类问题,其实很简单,在适当的位置调用thread.sleep(20)来释放所占有cpu资源,不要小看这20毫秒的睡眠,它的作用可是巨大的,可以使其他线程得到cpu资源,从而使你的cpu使用效率降下来。
看完前面的三个问题的解释,对于如何做似乎没有给出一个明确的答案,为了更好地说明如何解决这三个问题,我用一个比较完整的例子展现给大家,代码如下。
复制代码 代码如下:
//--------------------------- sub-thread class ---------------------------------------
//------------------------------------------------------------------------------------
//---file: clssubthread
//---description: the sub-thread template class file
//---author: knight
//------------------------------------------------------------------------------------
//---------------------------{sub-thread class}---------------------------------------
namespace threadtemplate
{
using system;
using system.threading;
using system.io;
/// <summary>
/// summary description for clssubthread.
/// </summary>
public class clssubthread:idisposable
{
private thread thdsubthread = null;
private mutex munique = new mutex();
private bool blnisstopped;
private bool blnsuspended;
private bool blnstarted;
private int nstartnum;
public bool isstopped
{
get{ return blnisstopped; }
}
public bool issuspended
{
get{ return blnsuspended; }
}
public int returnvalue
{
get{ return nstartnum;}
}
public clssubthread( int startnum )
{
//
// todo: add constructor logic here
//
blnisstopped = true;
blnsuspended = false;
blnstarted = false;
nstartnum = startnum;
}
/// <summary>
/// start sub-thread
/// </summary>
public void start()
{
if( !blnstarted )
{
thdsubthread = new thread( new threadstart( subthread ) );
blnisstopped = false;
blnstarted = true;
thdsubthread.start();
}
}
/// <summary>
/// thread entry function
/// </summary>
private void subthread()
{
do
{
// wait for resume-command if got suspend-command here
munique.waitone();
munique.releasemutex();
nstartnum++;
thread.sleep(1000); // release cpu here
}while( blnisstopped == false );
}
/// <summary>
/// suspend sub-thread
/// </summary>
public void suspend()
{
if( blnstarted && !blnsuspended )
{
blnsuspended = true;
munique.waitone();
}
}
/// <summary>
/// resume sub-thread
/// </summary>
public void resume()
{
if( blnstarted && blnsuspended )
{
blnsuspended = false;
munique.releasemutex();
}
}
/// <summary>
/// stop sub-thread
/// </summary>
public void stop()
{
if( blnstarted )
{
if( blnsuspended )
resume();
blnstarted = false;
blnisstopped = true;
thdsubthread.join();
}
}
#region idisposable members
/// <summary>
/// class resources dispose here
/// </summary>
public void dispose()
{
// todo: add clssubthread.dispose implementation
stop();//stop thread first
gc.suppressfinalize( this );
}
#endregion
}
}
那么对于调用呢,就非常简单了,如下:
// create new sub-thread object with parameters
clssubthread mysubthread = new clssubthread( 5 );
mysubthread.start();//start thread
thread.sleep( 2000 );
mysubthread.suspend();//suspend thread
thread.sleep( 2000 );
mysubthread.resume();//resume thread
thread.sleep( 2000 );
mysubthread.stop();//stop thread
//get thread's return value
debug.writeline( mysubthread.returnvalue );
//release sub-thread object
mysubthread.dispose();
在回过头来看看前面所说的三个问题。
对于问题一来说,首先需要局部成员的支持,那么
private mutex munique = new mutex();
private bool blnisstopped;
private bool blnsuspended;
private bool blnstarted;
光看成员名称,估计大家都已经猜出其代表的意思。接下来需要修改线程入口函数,要是这些开关变量能发挥作用,那么看看subthread这个函数。
/// <summary>
/// thread entry function
/// </summary>
private void subthread()
{
do
{
// wait for resume-command if got suspend-command here
munique.waitone();
munique.releasemutex();
nstartnum++;
thread.sleep(1000);
}while( blnisstopped == false );
}
函数比较简单,不到十句,可能对于“blnisstopped == false”这个判断来说,大家还比较好理解,这是一个普通的判断,如果当前stop开关打开了,就停止循环;否则一直循环。
大家比较迷惑的可能是如下这两句:
munique.waitone();
munique.releasemutex();
这两句的目的是为了使线程在suspend操作的时候能发挥效果,为了解释这两句,需要结合suspend和resume这两个方法,它俩的代码如下。
/// <summary>
/// suspend sub-thread
/// </summary>
public void suspend()
{
if( blnstarted && !blnsuspended )
{
blnsuspended = true;
munique.waitone();
}
}
/// <summary>
/// resume sub-thread
/// </summary>
public void resume()
{
if( blnstarted && blnsuspended )
{
blnsuspended = false;
munique.releasemutex();
}
}
为了更好地说明,还需要先简单说说mutex类型。对于此类型对象,当调用对象的waitone之后,如果此时没有其他线程对它使用的时候,就立刻获得信号量,继续执行代码;当再调用releasemutex之前,如果再调用对象的waitone方法,就会一直等待,直到获得信号量的调用releasemutex来进行释放。这就好比卫生间的使用,如果没有人使用则可以直接使用,否则只有等待。
明白了这一点后,再来解释这两句所能出现的现象。
复制代码 代码如下:
munique.waitone();
munique.releasemutex();
当在线程函数中,执行到“munique.waitone();”这一句的时候,如果此时外界没有发送suspend消息,也就是信号量没有被占用,那么这一句可以立刻返回。那么为什么要紧接着释放呢,因为不能总占着信号量,立即释放信号量是避免在发送suspend命令的时候出现等待;如果此时外界已经发送了suspend消息,也就是说信号量已经被占用,此时“munique.waitone();”不能立刻返回,需要等到信号量被释放才能继续进行,也就是需要调用resume的时候,“munique.waitone();”才能获得信号量进行继续执行。这样才能达到真正意义上的suspend和resume。
至于线程的start和stop来说,相对比较简单,这里我就不多说了。
现在再来分析一下问题二,其实例子比较明显,是通过构造函数和属性来完成参数和返回值,这一点我也不多说了。如果线程参数比较多的话,可以考虑属性来完成,类似于返回值。
问题三,我就更不用多说了。有人说了,如果子线程中的循环不能睡眠怎么办,因为睡眠的话,有时会造成数据丢失,这方面的可以借鉴前面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窗体的效果可能更好。
线程的四个相关问题已经说完了,这篇文章只说了单线程,以及单线程与ui线程交互的问题。其中涉及到的方法不一定是唯一的,因为.net还提供了其他类来扶助线程操作,这里就不一一罗列。