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();//模拟处理需要长时间做的任务
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();//模拟处理需要长时间做的任务
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();
watch.Stop();
Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/);
mre.Set();//告诉主线程子线程执行完了,如果不给ManualResetEvent实例调用这个方法,主线程会一直等待子线程调用ManualResetEvent实例的Set方法
}
如果子线程不调用Set方法,子线程代码如下:
/// <summary>
/// 辅助线程一
/// </summary>
static void eowOne(ManualResetEvent mre)
{
var watch = Stopwatch.StartNew();
Thread.Sleep();
watch.Stop();
Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/);
}
子线程做完了它的事情,但是没有调用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();
watch.Stop();
Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/);
mre.Set();
} /// <summary>
/// 辅助线程二
/// </summary>
static void eowTwo(ManualResetEvent mre)
{
var watch = Stopwatch.StartNew();
Thread.Sleep();
watch.Stop();
Console.WriteLine("辅助线程二做完了它的事,耗时:{0}", watch.ElapsedMilliseconds / );
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();
watch.Stop();
Console.WriteLine("辅助线程一做完了它的事,耗时:{0}", watch.ElapsedMilliseconds/);
mre.Set();
} /// <summary>
/// 辅助线程二
/// </summary>
static void eowTwo(AutoResetEvent mre)
{
var watch = Stopwatch.StartNew();
Thread.Sleep();
watch.Stop();
Console.WriteLine("辅助线程二做完了它的事,耗时:{0}", watch.ElapsedMilliseconds / );
var status = mre.Set();
if (status)
{
mre.Reset();
}
}
C# 多线程学习系列四之ThreadPool取消、超时子线程操作以及ManualResetEvent和AutoResetEvent信号量的使用的更多相关文章
- .NET并行与多线程学习系列一
并行与多线程学习系列一 一.并行初试: public static void test() { ; i < ; i++) { Console.WriteLine(i); } } public s ...
- Java多线程学习(四)等待/通知(wait/notify)机制
转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79690279 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...
- scrapy爬虫学习系列四:portia的学习入门
系列文章列表: scrapy爬虫学习系列一:scrapy爬虫环境的准备: http://www.cnblogs.com/zhaojiedi1992/p/zhaojiedi_python_00 ...
- DocX开源WORD操作组件的学习系列四
DocX学习系列 DocX开源WORD操作组件的学习系列一 : http://www.cnblogs.com/zhaojiedi1992/p/zhaojiedi_sharp_001_docx1.htm ...
- .net reactor 学习系列(四)---.net reactor应用场景
原文:.net reactor 学习系列(四)---.net reactor应用场景 前面已经学习了.net reactor一些基础知识,现在准备学习下实际的应用场景,只是简单的保护和 ...
- C# 多线程学习系列三之CLR线程池系列之ThreadPool
一.CLR线程池 1.进程和CLR的关系一个进程可以只包含一个CLR,也可以包含多个CLR2.CLR和AppDomain的关系一个CLR可以包含多个AppDomain3.CLR和线程池的关系一个CLR ...
- C# 多线程学习系列二
一.关于前台线程和后台线程 1.简介 CLR中线程分为两种类型,一种是前台线程.另一种是后台线程. 前台线程:应用程序的主线程.Thread构造的线程都默认为前台线程 后台线程:线程池线程都为后台线程 ...
- Identity Server4学习系列四之用户名密码获得访问令牌
1.简介 Identity Server4支持用户名密码模式,允许调用客户端使用用户名密码来获得访问Api资源(遵循Auth 2.0协议)的Access Token,MS可能考虑兼容老的系统,实现了这 ...
- MVC3+EF4.1学习系列(四)----- ORM关系的处理
上篇文章 终于把基础的一些操作写完了 但是这些都是单表的处理 而EF做为一个ORM框架 就必须点说说对于关系的处理 处理好关系 才能灵活的运用EF 关于关系的处理 一般就是 一对一 一对多 ...
随机推荐
- jq页面加载分割截图
<script> $(document).ready(function() { if (!Array.prototype.forEach) { Array.prototype.forEac ...
- screen对象和history对象
history对象保存着用户上网的历史记录,从窗口被打开的那一刻开始算起 使用go()方法可以在用户的历史记录中任意跳转 history.go(-1);//后退一页 history.go(1);//前 ...
- Silverlight中图片显示
silverlight中显示一个图片有很多的中方法,xaml中的image控件或者自定编写程序来生成image控件. silverlight中显示的图片只能是Bitmap, JPG, PNG(64位颜 ...
- Qt_技巧_将Qt动态链接生成的exe与dll打包方法
刚开始接触Qt,发现mac平台直接release编译一下就能够直接生成.app文件,并且可以直接运行..app位于工程文件内. 同样发方法在Windows直接运行exe却出现缺失dll情况. 讲过网上 ...
- TCP、UDP网络通信
IP地址和端口号 端口号是用两个字节(16位的二进制数)表示的,它的取值范围是0~65535,其中,0~1023之间的端口号用于一些知名的网络服务和应用, 用户的普通应用程序需要使用1024以上的端口 ...
- bootstrap2.1相关文档
本节课我们主要学习一下 Bootstrap表格和按钮功能,通过内置的 CSS定义,显示各种丰富的效果. 一.表格 Bootstrap提供了一些丰富的表格样式供开发者使用. 1.基本格式 //实现基本的 ...
- 微擎开启redis memcache文档2
微擎开启redis memcache 2018年01月20日 14:39:54 luogan129 阅读数:2161更多 个人分类: 微信开发 版权声明:本文为博主原创文章,未经博主允许不得转载. ...
- 关于tableview下拉刷新崩溃的问题
逻辑应该是这样的:1. 下拉2. 达到下拉临界值以后再请求网络数据3. 待数据加载到本地以后才更新 data source4. reload tableview 如果先清空再下拉,后果就是往下拉的距离 ...
- Spring下配置几种常用连接池
1.连接池概述 数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出.对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标.数据库连接池正是 ...
- C++ OCCI API数据库操作之连接、返回查询结果集为json格式
使用C++操作数据库,转换返回结果集为json格式,易于解析. 以下程序的编译.运行环境:Windows 10 1803.VS2017 17.5.2(vc14).解决方案配置:Release.解决方案 ...