站长之家备案查询,招聘工作,网站建设的软文怎么写,如何提升网站知名度绝大多数情况下#xff0c;我们都应该使用CLR线程池#xff0c;而不是直接操作Thread#xff0c;本章节介绍直接操作线程池的ThreadPool#xff0c;但实际开发中也很少直接使用它。 一、CLR和线程池
1.1 CLR的主要工作
CLR#xff08;Common Language Runtime#xff0… 绝大多数情况下我们都应该使用CLR线程池而不是直接操作Thread本章节介绍直接操作线程池的ThreadPool但实际开发中也很少直接使用它。 一、CLR和线程池
1.1 CLR的主要工作
CLRCommon Language Runtime公共语言运行时是管理应用程序执行的运行时环境类似Java的JVM虚拟机。之所以叫公共语言是因为它支持多种语言编译为CLR执行的字节码尽管绝大多数情况下都是使用C#。它的工作主要包括
托管应用程序代码加载和管理程序集内存分配和垃圾回收类型安全和代码验证异常处理调试诊断工具集代码执行包括JIT编译将中间字节码编译为本地机器码和跨语言互操作安全管理使用CAS策略和授权机制限制代码可执行的操作权限AppDomain 比进程更轻量的隔离方式 有自己的安全策略、配置文件和垃圾回收现在较少使用了线程管理底层使用Windows操作系统进行管理和调度但CLR提供了线程管理的API
1.2 应用程序和CLR实例是一对一关系吗
正常情况下每个应用程序都托管在一个CLR实例中但由于.NET的技术发展历史存在一些不一样的情况
早期的AspNet应用托管在IIS中。IIS维护着一个应用线程池只有一个CLR实例。CLR创建多个AppDomain每个应用都在相互隔离的AppDomain中运行。所以这些应用共享着一个CLR实例。但是现在更加鼓励直接使用进程。现代的AspNetCore应用无论是使用Kestrel独立运行还是用IIS作为反向代理或是在容器中运行每个AspNetCore应用都拥有独立的进程和CLR实例已不再依赖于AppDomain。桌面应用和控制台应用 应用程序启动时CLR会创建一个默认的AppDomain。应用程序的所有代码和资源都会加载到这个默认的AppDomain中一个应用对应一个CLR实例。
1.3 CLR和线程池
创建和销毁线程的开销很大太多的线程不但浪费资源也会影响GC性能所在CLR管理着一个线程池。如果CLR实例中有AppDomain则所有AppDomain共享这个线程池。如果一个进程加载了多个CLR则每个CLR都有一个线程池。 CLR初始化时线程池中还没有线程。线程池维护着一个操作请求队列当应用程序执行一个异步操作时会将这个异步任务追加到队列中。线程池从队列中取出任务并派发给一个线程池中的线程。如果线程池中没有线程会自动创建一个新线程这和创建线程的开销没有什么大的区别。重要在于这个线程完成任务后并不会销毁而是返回线程池进入空闲状态等待响应另外一个请求。如果异步请求非常多线程池会自动扩充线程数量以适应繁忙的请求当请求减少时就会有比较多的线程空闲下来并进入休眠状态休眠时间超过一个阀值线程会自己醒来并干掉自己以释放资源。 正常情况下CLR会根据CPU的核数快速创建初始数量的线程以充分利用多核CPU的性能这通常也是线程池的最小线程数。System.Threading.ThreadPool类提供了几个静态方法可以获取和设置线程池的最小和最大数量GetMaxThreads、SetMaxThreads、GetMinThreads、SetMinThreads。但强列建议不要去设置处理这些问题CLR比我们更聪明。
1.4 线程池的调度原理
CLR线程池的调度工作原理如下所示
主线程将线程池异步任务使用ThreadPool、Task、Timer等创建的异步任务放入到CLR线程池的全局队列中。工作者线程以下称之为异步线程按先入先出的规则从全局队列中取出异步任务这时可能出现多个异步线程同时取同一个任务的情况为以保障多个线程不会取到同一个任务所有线程都要先竞争这个任务的线程同步锁。锁定后其它线程就不会再竞争这个任务。当异步线程取出任务后会放入属于自己的本地队列中再按后入先出的规则取出任务进行处理。由于本地队列只属于自己所以此时不需要同步锁。如果异步线程发现它的本地队列空了会尝试从另一个异步线程的本地队列的尾部中“偷”一个任务此时会请求获取这个任务的线程同步锁。当所有本地队列都空了就会尝试从全局队列中取任务如果也空了就会进入睡眠状态。如果睡眠太长时间会自己醒来并销毁自身GC会在适当时候回收线程使用的资源。 注意CLR线程池不仅只有工作者线程还有I/O线程将在I/O异步操作章节详述
二、使用ThreadPool
2.1 使用ThreadPool创建(线程池)线程
//以下分别使用传入方法和传入Lambda的方式创建线程
//使用QueueUserWorkItem()方法创建结合上节原理Queue很贴切
public class Program
{static void Main(){var ct Thread.CurrentThread;Console.WriteLine(${ct.ManagedThreadId}:主线程开始);//方式1传入方法ThreadPool.QueueUserWorkItem(NoParamWorker);//方式2传入Lambda//ThreadPool.QueueUserWorkItem((state){...},实参)state为形参ThreadPool.QueueUserWorkItem((state) {var ct Thread.CurrentThread;Console.WriteLine(${ct.ManagedThreadId}:Worker拿到参数{state});},5);}static void NoParamWorker(object state){var ct Thread.CurrentThread;Console.WriteLine(${ct.ManagedThreadId}:Worker执行不传参的任务);}
}
/*输出
1:主线程开始
6:Worker线程执行不传参的任务
7:Worker线程拿到参数5
*/2.2 使用CancellationTokenSource终止线程
上节我们使用共享变量来终止线程这节使用终止线程的专用对象CancellationTokenSource两者原理和用法相似。直白的说就是向线程传入一个信号令牌Token可以在线程外部发送终止信号Cancel线程收到终止信号后Token.IsCancellationRequested由自己来终止线程。外部直接终止线程是危险的操作之前可以用的Abort()现在已经不能使用。只能传入信号由线程自己终止自己这样可以安全的释放内存。
//1、在异步线程中循环输出数字然后在主线程中终止异步线程
public class Program
{static void Main(){//创建Token信号令牌源一个源可以关联多个令牌var cts new CancellationTokenSource();//创建线程并传入Token信号令牌ThreadPool.QueueUserWorkItem((state) CountWorker(cts.Token, 10000) );Console.WriteLine(输入Enter终止异步操作);Console.ReadLine();//发送终止信息cts.Cancel(); Console.ReadLine();}static void CountWorker(CancellationToken token, int num){var ct Thread.CurrentThread;Console.WriteLine(${ct.ManagedThreadId}:Worker线程开始执行任务);for (int i 0; i num; i){if (!token.IsCancellationRequested) //判断是否收到外部的终止信号{Console.WriteLine(i);Thread.Sleep(200);//模拟耗时等待}else {Console.WriteLine(任务被终止了);break;//退出循环}}}
}//2、CancellationTokenSource的其它API
//2.1 注册异步线程终止后的回调可以注册多个
var cts new CancellationTokenSource();
cts.Token.Register(() { Console.WriteLine(终止回调1); });
cts.Token.Register(() { Console.WriteLine(终止回调2); });//2.2 调用上例的CountWorker不更改方法签名的情况下屏蔽外部的终止信息
//此时外部调用【cts.Cancel();】将无法终止线程
ThreadPool.QueueUserWorkItem((state)CountWorker(cts.Token.None, 10000));//2.3 关联多个cts略
//CancellationTokenSource.CreateLindedTokenSource(cts1.Token,cts2.Token)
2.3 多线程的执行上下文
执行上下文文字上理解是一个比较抽象的概念。但是在数据层面它本质是一个包含着多层嵌套的复杂对象记录着当前环境的一些信息 在程序流转时各个环节都可以读取或设置这个对象。如果做.NET后端最熟悉的上下文应该就是HttpContext。 执行上下文一般包括安全设置、宿主信息、上下文数据等具体内容根据不同运行环境会有差异。当CLR初始化时会为主线程创建执行上下文默认情况下当一个线程主线程使用另外一个线程辅线程时前者的执行上下文会流向复制辅线程。如果在辅助线程里再开辅助线程也是遵循这样的规律。 绝大多数情况下按默认方式传递是最优的但这种流向还是会消耗一些性能。如果想极致提升性能而辅助线程又用不到执行上下文可以手动阻止上下文流动。 大概知道咋回事就行了例子就不举了。
三、后记
我们很少直接使用ThreadPool因为它有一些限制比如你无法知道异步任务什么时候完成也无法让异步操作返回值。而System.Threading.Tasks命名空间下的类型能够解决这些技术问题它建立在ThreadPool基础之上仍然是使用CLR线程池。 *这是一个系列文章将全面介绍多线程、用户态协程和单线程事件循环机制建议收藏、点赞哦 *你在并发编程过程中碰到了哪些难题欢迎评论区交流~~~ 我是functionMC function MyClass(){…} C#/TS/鸿蒙/AI等技术问题以及如何写Bug、防脱发、送外卖等高深问题都可以私信提问哦