C# 多线程学习系列四之ThreadPool取消、超时子线程操作以及ManualResetEvent和AutoResetEvent信号量的使用
1、简介
虽然ThreadPool、Thread能开启子线程将一些任务交给子线程去承担,但是很多时候,因为某种原因,比如子线程发生异常、或者子线程的业务逻辑不符合我们的预期,那么这个时候我们必须关闭它,而不是让它继续执行,消耗资源.让CPU不在把时间和资源花在没有意义的代码上.
2、主线程取消所有子线程执行的简单代码演示和原理分析
(1)、代码演示
static void Main(string[] args) { //显示定义一个取消辅助线程的操作 CancellationTokenSource ctsToken = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(o => EoworkOne(ctsToken.Token)); ThreadPool.QueueUserWorkItem(o => EoworkTwo(ctsToken.Token)); ctsToken.Cancel(); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> /// <param name="token"></param> static void EoworkOne(CancellationToken token) { //判断主线程是否调用了CancellationTokenSource实例的Cancel方法 //相当于判断主线程是否传递给辅助线程一一个取消标记,如果你去看源码,你会发现,里面有个有趣的类Timer,so,你懂的!结合之前的文档,可以猜测这个时间很有可能是CPU切换上线文的时间
//每当过了这个时间,该子线程就去判断主线程有没有传递给它取消的信号.当然这只是我的猜测,哈哈
if (token.IsCancellationRequested) { //如果主线程传递给辅助线程一一个取消操作标记,执行下面的代码 Console.WriteLine("主线程调用了Cancel方法,所以辅助线程一获取了主线程取消辅助线程一的标记,但是并不会真正的关闭当前线程"); Console.WriteLine("辅助线程一执行return操作,自己显示的退出,那么接下去的方法都不会被执行"); return; } } /// <summary> /// 辅助线程二 /// </summary> /// <param name="token"></param> static void EoworkTwo(CancellationToken token) { //判断主线程是否调用了CancellationTokenSource实例的Cancel方法 //相当于判断主线程是否传递给辅助线程一一个取消标记 if (token.IsCancellationRequested) { //如果主线程传递给辅助线程一一个取消操作标记,执行下面的代码 Console.WriteLine("主线程调用了Cancel方法,所以辅助线程二获取了主线程取消辅助线程二的标记,但是并不会真正的关闭当前线程"); } //因为当主线程传递给辅助线程二一个取消标记,但是上面的if语句块,并没有执行return操作,所以下面的语句还是会继续执行 Console.WriteLine("辅助线程二获得取消标记操作后,并没有执行显示的return操作,所以辅助线程二继续执行"); }
(2)、原理分析
第一步:创建一个CancellationTokenSource对象实例,该对象包含了所有关于取消子线程有关的所有状态
CancellationTokenSource ctsToken = new CancellationTokenSource();
第二步:将CancellationTokenSource对象实例的CancellationToken对象实例传递给需要进行取消操作的所有子线程.并且可以通过这个CancellationToken对象实例关联到CancellationTokenSource对象实例.
ThreadPool.QueueUserWorkItem(o => EoworkOne(ctsToken.Token)); ThreadPool.QueueUserWorkItem(o => EoworkTwo(ctsToken.Token));
第三步:当主线程调用CancellationTokenSource对象实例的Cancel方法,所有的子线程通过调用CancellationToken对象实例的IsCancellationRequested属性,该属性定时去获取初始线程(主线程)是否执行了CancellationTokenSource对象实例的Cancel方法,如果调用了,该属性为true。这时可以理解为子线程到主线程的取消信号,可以通过调用return方法来终止子线程的操作.
//判断主线程是否调用了CancellationTokenSource实例的Cancel方法 //相当于判断主线程是否传递给辅助线程一一个取消标记 if (token.IsCancellationRequested) { //如果主线程传递给辅助线程一一个取消操作标记,执行下面的代码 Console.WriteLine("主线程调用了Cancel方法,所以辅助线程一获取了主线程取消辅助线程一的标记,但是并不会真正的关闭当前线程"); Console.WriteLine("辅助线程一执行return操作,自己显示的退出,那么接下去的方法都不会被执行"); return; }
3、如果创建一个不能被取消的子线程
通过给子线程传递一个CancellationToken.None实例,该子线程无法被取消,原因很简单,CancellationToken.None实例没有关联的CancellationTokenSource对象实例,所以无法调用Cancel方法显示取消.所以子线程调用token.IsCancellationRequested属性,该属性永远为false.调用token.CanBeCanceled属性也为false.
static void Main(string[] args) { ThreadPool.QueueUserWorkItem(o => EoworkOne(CancellationToken.None)); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> /// <param name="token"></param> static void EoworkOne(CancellationToken token) { if (token.IsCancellationRequested) { //永远无法执行 } Console.WriteLine("辅助线程一能被取消吗?{0}",token.CanBeCanceled?"能":"不能"); Console.WriteLine("通过CancellationToken.None实例创建的子线程无法被取消"); }
4、初始线程(主线程)调用给CancellationTokenSource对象实例的Cancel方法添加回调函数
通过调用CancellationToken实例的Register方法来实现这个功能.
static void Main(string[] args) { CancellationTokenSource ctsToken = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem((o => eowOne(ctsToken.Token))); ctsToken.Token.Register(() => { Console.WriteLine("ctsToken实例调用Cancel方法之后执行的回调函数一"); }); ctsToken.Token.Register(() => { Console.WriteLine("ctsToken实例调用Cancel方法之后执行的回调函数二"); }); ctsToken.Cancel(); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> static void eowOne(CancellationToken token) { Thread.Sleep(2000);//模拟处理需要长时间做的任务 Console.WriteLine("辅助线程一做完了它的事"); }
通过输出,可以发现,在给CancellationTokenSource实例的Token注册完回调函数后,调用CancellationTokenSource实例的Cancel方法,立刻执行回调函数,但是,主线程并没有等子线程执行完毕,在执行注册的回调.而是直接执行回调。说明线程池线程在管理子线程何时执行完毕是非常无力的.
5、关于处理CancellationTokenSource实例调用Cancel方法后,获取所有回调函数的未处理的异常
(1)、给CancellationTokenSource的Cancel方法传递true
static void Main(string[] args) { CancellationTokenSource ctsToken = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem((o => eowOne(ctsToken.Token))); ctsToken.Token.Register(() => { throw new Exception("回调函数一抛出的异常"); }); ctsToken.Token.Register(() => { throw new Exception("回调函数二抛出的异常"); }); ctsToken.Cancel(true); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> static void eowOne(CancellationToken token) { Thread.Sleep(2000);//模拟处理需要长时间做的任务 Console.WriteLine("辅助线程一做完了它的事"); }
调试代码发现,执行到第一个回调函数,抛出异常,程序直接跳出,不再执行第二个函数.所以可以得出结论,为Cancel方法传递true,它只会捕获第一个异常,不再执行第二个异常.
(2)、给CancellationTokenSource的Cancel方法传递false
传递false后,程序会分别执行所有的回调,并抛出一个System.AggregateException异常,回调函数的异常会被追加到到其InnerExceptions属性中.
6、ManualResetEvent、AutoResetEvent阻塞线程信号量使用
关于强制主线程等待子线程完成任务之后执行的方法主要用这两个信号量来实现,注意主线程只能等待一个子线程的完成,不能等待两个子线程完成,这里我试了很多种办法,都不行,可能对它的Api还不够了解.所以用的时候需要考虑这点.使用ManualResetEvent信号量,主线程只能等待一个子线程的完成.
用法如下:
(1)、ManualResetEvent
static void Main(string[] args) { ManualResetEvent mre = new ManualResetEvent(false);//创建ManualResetEvent信号量,主线这里构造函数必须传递false ThreadPool.QueueUserWorkItem((o => eowOne(mre)));//开启辅助线程 mre.WaitOne();//让主线程等待子线程的完成 Console.WriteLine("主线程继续做它的事情!"); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> static void eowOne(ManualResetEvent mre) { var watch = Stopwatch.StartNew(); Thread.Sleep(2000); watch.Stop(); Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/1000); mre.Set();//告诉主线程子线程执行完了,如果不给ManualResetEvent实例调用这个方法,主线程会一直等待子线程调用ManualResetEvent实例的Set方法 }
如果子线程不调用Set方法,子线程代码如下:
/// <summary> /// 辅助线程一 /// </summary> static void eowOne(ManualResetEvent mre) { var watch = Stopwatch.StartNew(); Thread.Sleep(2000); watch.Stop(); Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/1000); }
子线程做完了它的事情,但是没有调用ManualResetEvent实例的Set方法,所以,主线程会一直等待.这里主线程就被阻塞了.
结论:
(1)、当给ManualResetEvent实例的构造函数传false的时候,主线程调用ManualResetEvent实例的WaitOne方法时,如果子线程没有调用ManualResetEvent实例的Set方法,那么主线程会阻塞.
(2)、如果子线程调用了ManualResetEvent实例的Set方法,那么主线程调用ManualResetEvent实例的WaitOne方法,那么主线程会接收到一个子线程已经完成的信号,并且继续执行.不会阻塞.
(3)、无论怎么样主线程都会阻塞,只是不调用Set,主线程永远阻塞了,执行不下去了,调用Set,主线程还是会阻塞,但是当子线程完成工作之后,它会继续执行.
(2)、ManualResetEvent的ReSet方法
让ManualResetEvent实例回归初始状态
static void Main(string[] args) { ManualResetEvent mre = new ManualResetEvent(false);//创建ManualResetEvent信号量,主线这里构造函数必须传递false ThreadPool.QueueUserWorkItem((o => eowOne(mre)));//开启辅助线程一 mre.WaitOne();//让主线程等待辅助线程一的完成 mre.Reset();//调用ReSet方法,让ManualResetEvent回到初始状态,如果不使用这个方法,主线程不会等待辅助线程二,直接执行,因为辅助线程一已经调用了mre.Set方法 ThreadPool.QueueUserWorkItem((o => eowTwo(mre)));//开启辅助线程二 mre.WaitOne();//让主线程等待子线程辅助线程二的完成 Console.WriteLine("主线程继续做它的事情!"); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> static void eowOne(ManualResetEvent mre) { var watch = Stopwatch.StartNew(); Thread.Sleep(2000); watch.Stop(); Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/1000); mre.Set(); } /// <summary> /// 辅助线程二 /// </summary> static void eowTwo(ManualResetEvent mre) { var watch = Stopwatch.StartNew(); Thread.Sleep(2000); watch.Stop(); Console.WriteLine("辅助线程二做完了它的事,耗时:{0}", watch.ElapsedMilliseconds / 1000); var status = mre.Set(); if (status) { mre.Reset(); } }
ok,主线程会依次等待两个线程顺序执行完它们的事情,你可能发现一个问题.这和同步有什么区别!哈哈,有区别,如果主线程执行的任务足够耗时,而且执行到某一个时段,需要判断子线程是否完成,获取需要子线程的返回值(当然TreadPool不能很友好的拿到返回值),这个时候这种做法就有优势了两个线程各自承担自己的事情,互不干扰,需要协同操作了,主线程调用下Wait方法,确认子线程正确的完成了它的操作之后,继续执行主线程的任务..所以需谨慎使用.主线程如果啥都不干,光光去等待子线程完成,这种情况和同步就没有删么区别了.所以这个过程可能会卡界面.也有可能不卡.
(3)、AutoResetEvent信号量
AutoResetEvent和ManualResetEvent大体上没什么区别,都是阻塞主线程,但是ManualResetEvent需要每次调用ReSet方法而AutoResetEvent不用.
static void Main(string[] args) { AutoResetEvent mre = new AutoResetEvent(false);//创建ManualResetEvent信号量,主线这里构造函数必须传递false ThreadPool.QueueUserWorkItem((o => eowOne(mre)));//开启辅助线程一 mre.WaitOne();//让主线程等待辅助线程一的完成 ThreadPool.QueueUserWorkItem((o => eowTwo(mre)));//开启辅助线程二 mre.WaitOne();//让主线程等待子线程辅助线程二的完成 Console.WriteLine("主线程继续做它的事情!"); Console.Read(); } /// <summary> /// 辅助线程一 /// </summary> static void eowOne(AutoResetEvent mre) { var watch = Stopwatch.StartNew(); Thread.Sleep(2000); watch.Stop(); Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/1000); mre.Set(); } /// <summary> /// 辅助线程二 /// </summary> static void eowTwo(AutoResetEvent mre) { var watch = Stopwatch.StartNew(); Thread.Sleep(2000); watch.Stop(); Console.WriteLine("辅助线程二做完了它的事,耗时:{0}", watch.ElapsedMilliseconds / 1000); var status = mre.Set(); if (status) { mre.Reset(); } }