多线程同步代码示例

多线程情况下主要需解决两类问题:1、访问公共数据;2、控制线程执行顺序
一个进程中的多个线程都是可以访问其进程的其他资源,多线程若不加以控制也是并发执行的,若在多线程的执行方法中包含操作全局变量、者静态变量或是使用I/O设备的时候,很容易的就会产生线程安全的问题,从而导致不可预估的错误。

一、普通线程的同步方式:
1、Join(控制顺序)

 
2、同步事件AutoResetEvent和 ManualResetEvent(控制顺序)
它们之间唯一不同的地方就是在激活线程之后,状态是否自动由终止变为非终止。AutoResetEvent自动变为非终止,就是说一个AutoResetEvent只能激活一个线程。而ManualResetEvent要等到它的Reset方法被调用,状态才变为非终止,在这之前,ManualResetEvent可以激活任意多个线程。

 
注:ManualResetEvent会给所有引用的线程都发送一个信号(多个线程可以共用一个ManualResetEvent,当ManualResetEvent调用Set()时,所有线程将被唤醒),而AutoResetEvent只会随机给其中一个发送信号(只能唤醒一个)。
这里的线程同步还可以使用委托与事件(推荐使用事件)来实现线程间的简单通讯,比如在某一线程执行到某一结点后,通过事件向另一个或者多个线程发送更多的信息。

3、Monitor和lock(使用公共资源)

 
4、补充
线程同步的方式还有很多,比如Mutex。还有很多的方法,以后用到的时候在研究吧。
  Mutex:Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能;不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。

二、线程池的同步方式
使用线程池并发执行任务同样会遇到线程安全的问题,一样需要进行同步,在涉及线程使用公共资源,Monitor、lock等方法与上述线程使用一样,同样能达到理想的效果,就不重复介绍了;但是对于控制执行顺序上,这个没有使用new线程来的自由。
1、使用线程池

 
2、同步事件(控制顺序)
在线程池中,没有Join方法,若想控制线程的执行顺序,我推荐使用主线程等待线程池任务执行完毕,阻塞主线程的方式,这里可以使用WaitHandle:
 在这里,给线程池每个相关的线程都创建一个AutoResetEvent,在执行完毕之后分别把属于自己的AutoResetEvent变为非终止,WaitHandle使用WaitAll方法阻塞主线程、等待所有的AutoResetEvent事件变为true,另外WaitHandle还有一个WaitAny方法阻塞,不过是只要其中一个线程结束,就会继续运行,不再阻塞。

 
注:
1、WaitHandle同样可以用于new创建线程的同步事件;
2、WaitHandle等待方法(WaitAll、WaitAny)的数组长度的数目必须少于或等于 64 个,为了解决此限制,有网友封装了一个类,比较好用:

三、Task
Task与线程或者说线程池关系紧密,可以说是基于线程池实现的,虽说任务最终还是要抛给线程去执行,但是Task仍然会比线程、线程池的开销要小,并且提供了可靠的API来控制线任务执行。
使用Task来执行的任务最终会交给线程池来执行,若该任务需要长时间执行,可以将其标记为LongRunning,这是便会单独去请求创建线程来执行该任务。

1、创建Task
Task的创建也存在两种方式,使用new或者使用静态工厂方式来创建:

 
2、参数与返回值
使用Task也可以传入参数(object类型)与返回值:

 
3、等待Task
可以使用Task实例的Wait方法来实现等待任务结束,也可以向多线程一样,使用WaitAll和WaitAny一样来等待多个任务结束,不过操作更为简单:
t.Wait();
Task.WaitAll(t1, t2 ...);
Task.WaitAny(t1, t2 ...);

4、取消Task
任务也是可以事先取消的,不过需要使用CancellationTokenSource:


上述代码就可以将线程2给取消掉,当然,Cancel方法可以自己找个合适的地方调用。

5、继续Task
在Task中,可以实现在一个任务结束后开启另一个新的任务:

 
三、关于线程的知识
1、线程有前台线程和后台线程之分,使用new创建的线程默认为前台线程(可以使用IsBackground属性来进行更改),线程池里面都是后台线程前台线程:
前台线程是不会被立即关闭的,它的关闭只会发生在自己执行完成时,不受外在因素的影响。假如应用程序退出,造成它的前台线程终止,此时CLR仍然保持活动并运行,使应用程序能继续运行,当它的的前台线程都终止后,整个进程才会被销毁。
后台线程:后台线程是可以随时被CLR关闭而不引发异常的,也就是说当后台线程被关闭时,资源的回收是立即的,不等待的,也不考虑后台线程是否执行完成,就算是正在执行中也立即被终止。

2、线程被系统调度到CPU执行时存在优先级:这里的优先级不是优先执行,而是被调度到CPU执行的概率高;使用new创建线程与线程池的优先级默认都是Normal,不过前者可以通过Priority属性来设置优先级。优先级有5个级别:Highest、AboveNormal、Normal、BelowNormal和Lowest。

3、线程存在Suspend与Resume这两个过时的方法,但不是代表不能使用,只是微软不推荐你用,MSDN给出的原因是:请不要使用 Suspend 和 Resume 方法来同步线程活动。 没有办法知道当你暂停执行线程什么代码。 如果在安全权限评估期间持有锁,您挂起线程中的其他线程 AppDomain 可能被阻止。 如果执行类构造函数时,您挂起线程中的其他线程 AppDomain 中尝试使用类被阻止。 可以很容易发生死锁。你可以无视这个警告继续使用这两个方法进行线程同步,若觉得不怎么靠谱,那么可以在线程代码加入判断来保证执行正确性,或者使用控制同步事件(AutoResetEvent等)来实现线程同步。

4、线程池的线程很珍贵,因为数量是有限的,所以不适合执行长时间的作业任务,适合执行短期并且频繁的作业任务,若想执行长时间的作业任务,建议使用new创建新线程的方式。毕竟线程池设计的初衷就是为了解决频繁创建与撤销线程而造成的资源浪费。

5、使用传统线程方式来进行多线程编程的时候,对线程的控制总是不到位,产生一些奇奇怪怪的问题;或是代码写得很杂乱;或是开发人员乱用线程,比方说无限制的创建线程、将线程池线程占满,等等。
Task的出现,实现对传统线程操作的封装,提供可靠高效的API来控制线程,极大的方便多线程编程,所以可以用到Task的地方尽量使用Task;当然,这里仍会产生线程安全的问题,同样需要进行线程同步。

本文内容来源:
http://www.cnblogs.com/nbclw/p/8397657.html
http://www.cnblogs.com/nbclw/p/8441317.html

posted @   skybirdzw  阅读(255)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示