C#多线程编程.docx
- 文档编号:10273358
- 上传时间:2023-05-24
- 格式:DOCX
- 页数:62
- 大小:102.84KB
C#多线程编程.docx
《C#多线程编程.docx》由会员分享,可在线阅读,更多相关《C#多线程编程.docx(62页珍藏版)》请在冰点文库上搜索。
C#多线程编程
C#多线程编程
1、线程的启动
在实例化Thread的实例时,需要提供一个委托,在实例化这个委托时所用到的参数是线程将来启动时要运行的方法。
在.net中提供了两种启动线程的方式,一种是不带参数的启动方式,另一种是带参数的启动的方式。
1.1不带参数的启动方式
如果启动参数时无需其它额外的信息,可以使用ThreadStart来实例化Thread,见项目ThreadStart
(1):
程序的运行效果我们不用运行也会知道,那就是在循环中将系统当前时间的毫秒部分输出出来,在每次输出之后会将当前线程暂停一下,直到10次之后运行完毕,终止线程的执行。
在上面的代码中我们是通过定义全局变量的方法来指定线程暂停间隔,按照这种方法,假如要运行10个线程,每个线程的暂停间隔不一样的话,就需要定义10个全局变量,虽然最终不影响系统的运行效果,但是总觉得不是太爽。
有没有比较简单一点的办法呢?
有!
那就是使用带参数的启动方法。
1.2带参数的启动方法
如果要在实例化线程时要带一些参数,就不能用ThreadStart委托作为构造函数的参数来实例化Thread了,而要使用ParameterizedThreadStart委托,和ThreadStart一样的是它也是线程启动时要执行的方法,和ThreadStart不同的是,它在实例化时可以用一个带有一个Object参数的方法作为构造函数的参数,而实例化ThreadStart时所用到的方法是没有参数的。
为什么是Object这样的参数呢?
很简单,因为在.net中Object是所有类型的基类,用它可以表示Array(数组)、Interface(接口)、ValueType(值类型,如bool,byte,char,short,int,float,long,double等)、class(类)等.net中的类型。
当然,这也意味着如果你要启动一个线程,给它传递一个int类型参数时,必须在启动方法中进行相应的类型转换。
下面就是一个例子,在启动线程时指定了线程的暂停间隔,见项目ThreadStart_WithPara。
在这个方法里,我们在启动线程时顺便指定了线程的暂停间隔,也就是这句:
parameterThread.Start(30);
线程启动时运行的方法是publicvoidParameterRun(objectms),这个值为30的int类型变量被装箱成object,所以在方法中还需要将它转换成int类型,这个可以通过拆箱或者其它办法解决。
假如我们要启动两个线程,每个线程的暂停间隔不一样,见项目ThreadStart_DiffPara:
对上面的代码做一点说明,就是线程启动之后,线程的实例不必再存在,例如在上面的代码中我用的是同一个实例实例化了两个线程,并且这两个线程运行很正常。
1.3继续探索
上面解决了一个问题,如果在启动线程时需要参数如何解决,如果针对上面的问题继续发掘,比如:
在启动线程时不但要指定线程的暂停间隔,还需要指定循环次数(在上面的所有例子中都是执行10次的),这个问题该如何解决呢?
有两种办法可以解决:
首先可以继续在ParameterizedThreadStart这里做文章,因为这里可以使用一个Object类型的参数,那么可以通过数组或者一个类来解决(因为它们都是Object的子类)。
我在做某个系统时确实采用数组处理过这种情况,这样就要求在线程启动方法中必须清楚知道数组中每个参数的用途,不是太方便。
这里说说重新定义一个实体类来解决的方法,代码如下。
见项目:
ThreadStart(3)
第二种方法和上面方法有些相似,也是需要引入外部类,并且将Thread实例放在引入的类中,这种情况适合于在线程中处理的业务逻辑比较复杂的情况。
在前不久处理的一个项目中我用过这种情况,它是用来实现双向数据传输的。
如果实现上面的效果,代码如下:
StartThread(4)
上面的代码的运行效果和前面的代码运行效果类似,只不过是将业务处理代码放在一个单独的类MyThreadParameter中,使得MyThreadParameter看起来也像一个Thread,实际上维护的还是其内部的Thread,在一些大型系统中这样做的好处是便于维护。
总结:
在本篇主要讲述如何启动线程的问题,在启动时可能会遇到无需参数、需要多个参数的情况,在这里讲述了如何解决这些问题的思路。
在.net类库中虽然存在着庞大的类库,但是并不是总会有合适的类来解决我们所遇到的问题,但是只要肯动脑筋总会想到合适的办法。
2、线程的同步
在《多线程编程》系列第一篇讲述了如何启动线程,这篇讲述线程之间存在竞争时如何确保同步并且不发生死锁。
线程不同步引出的问题
下面做一个假设,假设有20张票,由三两个线程来实现一个售票程序,每次线程运行时首先检查是否还有票未售出,如果有就按照票号从小到大的顺序售出票号最小的票,程序的代码如下:
见项目:
SellTickets
(1)此为一个简单的售票程序
项目:
SellTickets
(2)这段程序的执行效果并不每次都一样。
从上图可以看出票号为001的号被售出了两次,为什么会出现这种情况呢?
请看代码③处:
ticketList.RemoveAt(0);//③
在某个情况有可能线程1恰好运行到此处,从ticketList中取出索引为0的那个元素并将票号输出,不巧的是正好分给线程1执行的时间片已用完,线程1进入休眠状态,线程2从头开始执行,它可以从容地从ticketList中取出索引为0的那个元素并且将其输出,因为线程1执行的时候虽然输出了ticketList中索引为0的那个元素但是来不及将其删除,所以这时候线程2得到的值和上次线程1得到的值一致,这就出现了有些票被售出了两次、有些票可能根本就没有售出的情况。
出现这种情况的根本原因就是多个线程都是对同一资源进行操作所致,所以在多线程编程应尽可能避免这种情况,当然有些情况下确实避免不了这种情况,这就需要对其采用一些手段来确保不会出现这种情况,这就是所谓的线程的同步。
在C#中实现线程的同步有几种方法:
lock、Mutex、Monitor、Semaphore、Interlocked和ReaderWriterLock等。
同步策略也可以分为同步上下文、同步代码区、手动同步几种方式。
2.1同步上下文
同步上下文的策略主要是依靠SynchronizationAttribute类来实现。
例如下面的代码就是一个实现了上下文同步的类的代码:
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Text;
//需要添加对System.EnterpriseServices.dll这个类库的引用采用使用这个dll
usingSystem.EnterpriseServices;
namespaceStartThread
{
[Synchronization(SynchronizationOption.Required)]//确保创建的对象已经同步
publicclassSynchronizationAttributeClass
{
publicvoidRun()
{
}
}
}
所有在同一个上下文域的对象共享同一个锁。
这样创建的对象实例属性、方法和字段就具有线程安全性,需要注意的是类的静态字段、属性和方法是不具有线程安全性的。
2.2同步代码区
同步代码区是另外一种策略,它是针对特定部分代码进行同步的一种方法。
2.2.1lock同步
针对上面的代码,要实现不会出现混乱(两次卖出同一张票或者有些票根本就没有卖出),可以lock关键字来实现,出现问题的部分就是在于判断剩余票数是否大于0,如果大于0则从当前总票数中减去最大的一张票,因此可以对这部分进行处理,代码如下:
privatevoidRun()
{
while(ticketList.Count>0)//①
{
lock(objLock)
{
if(ticketList.Count>0)
{
stringticketNo=ticketList[0];//②
Console.WriteLine("{0}:
售出一张票,票号:
{1}",Thread.CurrentThread.Name,ticketNo);
ticketList.RemoveAt(0);//③
Thread.Sleep
(1);
}
}
}
}
见项目SellTickets
(2)中修改部分
经过这样处理之后系统的运行结果就会正常。
总的来说,lock语句是一种有效的、不跨越多个方法的小代码块同步的做法,也就是使用lock语句只能在某个方法的部分代码之间,不能跨越方法。
2.2.2Monitor类
针对上面的代码,如果使用Monitor类来同步的话,代码则是如下效果:
privatevoidRun()
{
while(ticketList.Count>0)//①
{
Monitor.Enter(objLock);
if(ticketList.Count>0)
{
stringticketNo=ticketList[0];//②
Console.WriteLine("{0}:
售出一张票,票号:
{1}",Thread.CurrentThread.Name,ticketNo);
ticketList.RemoveAt(0);//③
Thread.Sleep
(1);
}
Monitor.Exit(objLock);
}
}
当然这段代码最终运行的效果也和使用lock关键字来同步的效果一样。
比较之下,大家会发现使用lock关键字来保持同步的差别不大:
”lock(objLock){“被换成了”Monitor.Enter(objLock);”,”}”被换成了”Monitor.Exit(objLock);”。
实际上如果你通过其它方式查看最终生成的IL代码,你会发现使用lock关键字的代码实际上是用Monitor来实现的。
如下代码:
lock(objLock){
//同步代码
}
实际上是相当于:
try{
Monitor.Enter(objLock);
//同步代码
}
finally
{
Monitor.Exit(objLock);
}
我们知道在绝大多数情况下finally中的代码块一定会被执行,这样确保了即使同步代码出现了异常也仍能释放同步锁。
Monitor类除了Enter()和Exit()方法之外,还有Wait()和Pulse()方法。
Wait()方法是临时释放当前获得的锁,并使当前对象处于阻塞状态,Pulse()方法是通知处于等待状态的对象可以准备就绪了,它一会就会释放锁。
下面我们利用这两个方法来完成一个协同的线程,一个线程负责随机产生数据,一个线程负责将生成的数据显示出来。
下面是代码:
见项目Wait_Pulse
执行上面的代码在大部分情况下会看到如下所示的结果:
一般情况下会看到上面的结果,原因是t1的Start()方法在先,所以一般会优先获得执行,t1执行后首先获得对象锁,然后在循环中通过Monitor.Wait(lockObject)方法临时释放对象锁,t1这时处于阻塞状态;这样t2获得对象锁并且得以执行,t2进入循环后通过Monitor.Pulse(lockObject)方法通知等待同一个对象锁的t1准备好,然后在生成随机数之后临时释放对象锁;接着t1获得了对象锁,执行输出t2生成的数据,之后t1通过Monitor.Wait(lockObject)通知t2准备就绪,并在下一个循环中通过Monitor.Wait(lockObject)方法临时释放对象锁,就这样t1和t2交替执行,得到了上面的结果。
当然在某些情况下,可能还会看到如下的结果:
至于为什么会产生这个结果,原因其实很简单,尽管t1.Start()出现在t2.Start()之前,但是并不能就认为t1一定会比t2优先执行(尽管可能在大多数情况下是),还要考虑线程调度问题,使用了多线程之后就会使代码的执行顺序变得复杂起来。
在某种情况下t1和t2对锁的使用产生了冲突,形成了死锁,也就出现了如上图所示的情况,为了避免这种情况可以通过让t2延时一个合适的时间。
2.4手控同步
手控同步是指使用不同的同步类来创建自己的同步机制。
使用这种策略要求手动地为不同的域或者方法同步。
2.4.1ReaderWriterLock
ReaderWriterLock支持单个写线程和多个读线程的锁。
在任一特定时刻允许多个线程同时进行读操作或者一个线程进行写操作,使用ReaderWriterLock来进行读写同步比使用监视的方式(如Monitor)效率要高。
下面是一个例子,在例子中使用了两个读线程和一个写线程,见项目test_ReaderWriterLock:
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
代码不对,与前面的重复了。
程序的执行效果如下:
2.4.2WaitHandle
WaitHandle类是一个抽线类,有多个类直接或者间接继承自WaitHandle类,类图如下:
在WaitHandle类中SignalAndWait、WaitAll、WaitAny及WaitOne这几个方法都有重载形式,其中除WaitOne之外都是静态的。
WaitHandle方法常用作同步对象的基类。
WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。
ØWaitHandle方法有多个Wait的方法,这些方法的区别如下:
ØWaitAll:
等待指定数组中的所有元素收到信号。
ØWaitAny:
等待指定数组中的任一元素收到信号。
ØWaitOne:
当在派生类中重写时,阻塞当前线程,直到当前的WaitHandle收到信号。
这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。
下面的是一个MSDN中的例子,讲的是一个计算过程,最终的计算结果为第一项+第二项+第三项,在计算第一、二、三项时需要使用基数来进行计算。
在代码中使用了线程池也就是ThreadPool来操作,这里面涉及到计算的顺序的先后问题,通过WaitHandle及其子类可以很好地解决这个问题。
见项目test_WaitHandle
程序的运行结果如下:
Result=0.355650523270459.
Result=0.125205692112756.
当然因为引入了随机数,所以每次计算结果并不相同,这里要讲述的是它们之间的控制。
首先在Result(intseed)方法中讲计算基数、第一项、第二项及第三项的方法放到线程池中,要计算第一二三项时首先要确定基数,这些方法通过manualEvent.WaitOne()暂时停止执行,于是计算基数的方法首先执行,计算出基数之后通过manualEvent.Set()方法通知计算第一二三项的方法开始,在这些方法完成计算之后通过autoEvents数组中的AutoResetEvent元素的Set()方法发出信号,标识执行完毕。
这样WaitHandle.WaitAll(autoEvents)这一步可以继续执行,从而得到执行结果。
在上面代码中的WaitHandle的其它子类限于篇幅不在这里一一举例讲解,它们在使用了多少有些相似之处(毕竟是一个爹、从一个抽象类继承下来的嘛)。
3、线程池ThreadPool
在面向对象编程中,经常会面对创建对象和销毁对象的情况,如果不正确处理的话,在短时间内创建大量对象然后执行简单处理之后又要销毁这些刚刚建立的对象,这是一个非常消耗性能的低效行为,所以很多面向对象语言中在内部使用对象池来处理这种情况,以提高性能,比如在ADO.NET内部就允许使用数据库连接池来提高性能。
在多线程编程时也会遇到上面的情况,如果创建了过多的线程将会增加操作系统资源的占用,并且还要处理资源要求和潜在的占用冲突,并且使用了多线程之后将使代码的执行流程和资源竞争情况变得复杂,稍不留心就会产生bug。
在使用多线程编程时对需要同步的资源访问尤其需要注意,如系统资源(系统端口等)、共享资源(文件、窗口句柄等)、属于单个应用程序的资源(如全局、静态和实例字段或属性)。
针对上面的情况,我们可以使用线程池来解决上面的大部分问题,跟使用单个线程相比,使用线程池有如下优点:
1、缩短应用程序的响应时间。
因为在线程池中有现成的线程处于等待分配任务状态(只要没有超过线程池的最大上限),无需创建线程。
2、不必管理和维护生存周期短暂的线程,不用在创建时为其分配资源,在其执行完任务之后释放资源。
3、线程池会根据当前系统特点对池内的线程进行优化处理。
总之使用线程池的作用就是减少创建和销毁线程的系统开销。
在.NET中有一个类ThreadPool,它提供了线程池的管理。
ThreadPool是一个静态类,它没有构造函数,对外提供的函数也全部是静态的。
其中有一个QueueUserWorkItem方法,它有两种重载形式,如下:
publicstaticboolQueueUserWorkItem(WaitCallbackcallBack):
将方法排入队列以便执行。
此方法在有线程池线程变得可用时执行。
publicstaticboolQueueUserWorkItem(WaitCallbackcallBack,Objectstate):
将方法排入队列以便执行,并指定包含该方法所用数据的对象。
此方法在有线程池线程变得可用时执行。
QueueUserWorkItem方法中使用的WaitCallback参数表示一个delegate,它的声明如下:
publicdelegatevoidWaitCallback(Objectstate)
如果需要传递任务信息可以利用WaitCallback中的state参数,类似于ParameterizedThreadStart委托。
下面是一个ThreadPool的例子,见项目ThreadPool
(1):
在上面的代码中我们使用了线程池,并让它执行了两个任务,一个是列出系统当前所有环境变量的值,一个是列出系统当前运行的进程名和它们的启动时间。
当然,优点和缺点总是同时存在的,使用ThreadPool也有一些缺点,使用线程池有如下缺点:
1、一旦加入到线程池中就没有办法让它停止,除非任务执行完毕自动停止;
2、一个进程共享一个线程池;
3、要执行的任务不能有返回值(当然,线程中要执行的方法也是不能有返回值,如果确实需要返回值必须采用其它技巧来解决);
4、在线程池中所有任务的优先级都是一样的,无法设置任务的优先级;
5、不太适合需要长期执行的任务(比如在Windows服务中执行),也不适合大的任务;
6、不能为线程设置稳定的关联标识,比如为线程池中执行某个特定任务的线程指定名称或者其它属性。
如果我们要面临的情况正好是线程池的缺点,那么我们只好继续使用线程而不是线程池。
不过在某些情况下使用线程池确实可以带来很多方便的,比如在WEB服务器中,可以使用线程池来处理来自客户端的请求,可以以比较高的性能运行。
4、多线程与UI操作
为了让程序尽快响应用户操作,在开发Windows应用程序时经常会使用到线程。
对于耗时的操作如果不使用线程将会使UI界面长时间处于停滞状态,这种情况是用户非常不愿意看到的,在这种情况下我们希望使用线程来解决这个问题。
下面是一个使用多线程操作界面UI的代码:
privatevoidbtnThread_Click(objectsender,EventArgse)
{
Threadthread=newThread(newThreadStart(Run));
thread.Start();
}
privatevoidRun()
{
while(progressBar.Value { progressBar.PerformStep(); } } 程序的界面如下: 我们的本意是点击“启动”按钮来启动模拟一个操作,在进度条中显示操作的总体进度。 不过如果我们真的点击“启动”按钮会很失望,因为它会抛出一个System.InvalidOperationException异常,异常描述就是“线程间操作无效: 从不是创建控件‘progressBar1’的线程访问它。 ”如下图所示 4.1CheckForIllegalCrossThreadCalls属性 之所以会出现这样的情况是因为在.NET中做了限制,不允许在调试环境下使用线程访问并非它自己创建的UI控件,这么做可能是怕在多线程环境下对界面控件进行操作会出现不可预知的情况,如果开发者可以确认自己的代码操作界面不会出现问题,可以用比较简单的方法解决,那就是设置CheckForIllegalCrossThreadCalls这个静态属性,它默认是true,如果将其设为false的话,以后在多线程环境下操作界面也不会抛出异常了,我们上面的代码可以修改为: privatevoidbtnThread_Click(objectsender,EventArgse) { //指示是否对错误线程的调用,即是否允许在创建UI的线程之外访问线程 CheckForIllegalCrossThreadCalls=false; Threadthread=newThread(newThreadStart(Run)); thread.Start(); } privatevoidRun() { while(progressBar.Value { progressBar.PerformStep(); } } 这样再执行程序就不会抛出异常了。 不过使用上面的代码我们可能还有些犯嘀咕,毕竟是不允许直接在线程中操作界面的,那么我们还可以用Invoke方法。 4.2Invoke方法来操作界面 下面是一个例子: 见项目test_Invoke 这个方法的功能跟上面的操作是一样的,只不过不需要设置CheckForIllegalCrossThreadCalls属性,而且还不会抛出异常,当然除了上面的方法之外,还可以使用BackgroundWorker类来完成同样的功能。 4.3BackgroundWorker类操作界面 因为使用BackgroundWorker类操作UI界面的例子周公博客上已经有过例子,所以这里的例子代码注释比较简单,读者可以看周公以前的示例,这次所使用的代码示例如下: 参见项目Test_BackgroundWorker 当然,除了BackgroundWorker可以完成上面的功能之外,利用System.Windows.Forms.Timer类也能完场上面的功能,在test_BackgroundWorder项目中添加Timer组件,一个Button控件及相关代码。 总结: 本篇主要讲述了使用线程操作Windows应用程序界面的方法,这些方法在编写多线程的UI程序时可以参考。 5、一个简单的多线程的例子 在开发中经常会遇到线程的例子,如果某个后台操作比较费时间,
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C# 多线程 编程