.net线程池内幕

本文通过对.NET4.5的ThreadPool源码的分析讲解揭示.NET线程池的内幕,并总结ThreadPool设计的好与不足。

线程池的作用
线程池,顾名思义,线程对象池。Task和TPL都有用到线程池,所以了解线程池的内幕有助于你写出更好的程序。由于篇幅有限,在这里我只讲解以下核心概念:

  • 线程池的大小

  • 如何调用线程池添加任务

  • 线程池如何执行任务

Threadpool也支持操控IOCP的线程,但在这里我们不研究它,涉及到task和TPL的会在其各自的博客中做详解。

线程池的大小
不管什么池,总有尺寸,ThreadPool也不例外。ThreadPool提供了4个方法来调整线程池的大小:

  • SetMaxThreads

  • GetMaxThreads

  • SetMinThreads

  • GetMinThreads

SetMaxThreads指定线程池最多可以有多少个线程,而GetMaxThreads自然就是获取这个值。SetMinThreads指定线程池中最少存活的线程的数量,而GetMinThreads就是获取这个值。
为何要设置一个最大数量和有一个最小数量呢?原来线程池的大小取决于若干因素,如虚拟地址空间的大小等。比如你的计算机是4g内存,而一个线程的初始堆栈大小为1m,那么你最多能创建4g/1m的线程(忽略操作系统本身以及其他进程内存分配);正因为线程有内存开销,所以如果线程池的线程过多而又没有被完全使用,那么这就是对内存的一种浪费,所以限制线程池的最大数是很make sense的。
那么最小数又是为啥?线程池就是线程的对象池,对象池的最大的用处是重用对象。为啥要重用线程,因为线程的创建与销毁都要占用大量的cpu时间。所以在高并发状态下,线程池由于无需创建销毁线程节约了大量时间,提高了系统的响应能力和吞吐量。最小数可以让你调整最小的存活线程数量来应对不同的高并发场景。

如何调用线程池添加任务
线程池主要提供了2个方法来调用:QueueUserWorkItem和UnsafeQueueUserWorkItem。
两个方法的代码基本一致,除了attribute不同,QueueUserWorkItem可以被partial trust的代码调用,而UnsafeQueueUserWorkItem只能被full trust的代码调用。

 public static bool QueueUserWorkItem(WaitCallback callBack) 
{
StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
   
return ThreadPool.QueueUserWorkItemHelper(callBack, (object) null, ref stackMark, true);
}

QueueUserWorkItemHelper首先调用ThreadPool.EnsureVMInitialized()来确保CLR虚拟机初始化(VM是一个统称,不是单指java虚拟机,也可以指CLR的execution engine),紧接着实例化ThreadPoolWorkQueue,最后调用ThreadPoolWorkQueue的Enqueue方法并传入callback和true。

[SecurityCritical]

public void Enqueue(IThreadPoolWorkItem callback, bool forceGlobal)

{

ThreadPoolWorkQueueThreadLocals queueThreadLocals = (ThreadPoolWorkQueueThreadLocals) null;

if (!forceGlobal)

queueThreadLocals = ThreadPoolWorkQueueThreadLocals.threadLocals;

if (this.loggingEnabled)

FrameworkEventSource.Log.ThreadPoolEnqueueWorkObject((object) callback);

if (queueThreadLocals != null)

{

queueThreadLocals.workStealingQueue.LocalPush(callback);

}

else

{

ThreadPoolWorkQueue.QueueSegment comparand = this.queueHead;

while (!comparand.TryEnqueue(callback))

{

Interlocked.CompareExchange<ThreadPoolWorkQueue.QueueSegment>(ref comparand.Next, new ThreadPoolWorkQueue.QueueSegment(), (ThreadPoolWorkQueue.QueueSegment) null);

for (; comparand.Next != null; comparand = this.queueHead)

Interlocked.CompareExchange<ThreadPoolWorkQueue.QueueSegment>(ref this.queueHead, comparand.Next, comparand);

}

}

this.EnsureThreadRequested();

}

ThreadPoolWorkQueue主要包含2个“queue”(实际是数组),一个为QueueSegment(global work queue),另一个是WorkStealingQueue(local work queue)。两者具体的区别会在Task/TPL里讲解,这里暂不解释。
由于forceGlobal是true,所以执行到了comparand.TryEnqueue(callback),也就是QueueSegment.TryEnqueue。comparand先从队列的头(queueHead)开始enqueue,如果不行就继续往下enqueue,成功后再赋值给queueHead。
让我们来看看QueueSegment的源代码:

public QueueSegment()

{

this.nodes = new IThreadPoolWorkItem[256];

}


public bool TryEnqueue(IThreadPoolWorkItem node)

{

int upper;

int lower;

this.GetIndexes(out upper, out lower);

while (upper != this.nodes.Length)

{

if (this.CompareExchangeIndexes(ref upper, upper + 1, ref lower, lower))

{

Volatile.Write<IThreadPoolWorkItem>(ref this.nodes[upper], node);

return true;

}

}

return false;

}

这个所谓的global work queue实际上是一个IThreadPoolWorkItem的数组,而且限死256,这是为啥?难道是因为和IIS线程池(也只有256个线程)对齐?使用interlock和内存写屏障volatile.write来保证nodes的正确性,比起同步锁性能有很大的提高。最后调用EnsureThreadRequested,EnsureThreadRequested会调用QCall把请求发送至CLR,由CLR调度ThreadPool。

线程池如何执行任务
线程被调度后通过ThreadPoolWorkQueue的Dispatch方法来执行callback。

internal static bool Dispatch()

{

ThreadPoolWorkQueue threadPoolWorkQueue = ThreadPoolGlobals.workQueue;

int tickCount = Environment.TickCount;

threadPoolWorkQueue.MarkThreadRequestSatisfied();

threadPoolWorkQueue.loggingEnabled = FrameworkEventSource.Log.IsEnabled(EventLevel.Verbose, (EventKeywords) 18);

bool flag1 = true;

IThreadPoolWorkItem callback = (IThreadPoolWorkItem) null;

try

{

ThreadPoolWorkQueueThreadLocals tl = threadPoolWorkQueue.EnsureCurrentThreadHasQueue();

while ((long) (Environment.TickCount - tickCount) < (long) ThreadPoolGlobals.tpQuantum)

{

try

{

}

finally

{

bool missedSteal = false;

threadPoolWorkQueue.Dequeue(tl, out callback, out missedSteal);

if (callback == null)

flag1 = missedSteal;

else

threadPoolWorkQueue.EnsureThreadRequested();

}

if (callback == null)

return true;

if (threadPoolWorkQueue.loggingEnabled)

FrameworkEventSource.Log.ThreadPoolDequeueWorkObject((object) callback);

if (ThreadPoolGlobals.enableWorkerTracking)

{

bool flag2 = false;

try

{

try

{

}

finally

{

ThreadPool.ReportThreadStatus(true);

flag2 = true;

}

callback.ExecuteWorkItem();

callback = (IThreadPoolWorkItem) null;

}

finally

{

if (flag2)

ThreadPool.ReportThreadStatus(false);

}

}

else

{

callback.ExecuteWorkItem();

callback = (IThreadPoolWorkItem) null;

}

if (!ThreadPool.NotifyWorkItemComplete())

return false;

}

return true;

}

catch (ThreadAbortException ex)

{

if (callback != null)

callback.MarkAborted(ex);

flag1 = false;

}

finally

{

if (flag1)

threadPoolWorkQueue.EnsureThreadRequested();

}

return true;

}

while语句判断如果执行时间少于30ms会不断继续执行下一个callback。这是因为大多数机器线程切换大概在30ms,如果该线程只执行了不到30ms就在等待中断线程切换那就太浪费CPU了,浪费可耻啊!
Dequeue负责找到需要执行的callback:

public void Dequeue(ThreadPoolWorkQueueThreadLocals tl, out IThreadPoolWorkItem callback, out bool missedSteal)

{

callback = (IThreadPoolWorkItem) null;

missedSteal = false;

ThreadPoolWorkQueue.WorkStealingQueue workStealingQueue1 = tl.workStealingQueue;

workStealingQueue1.LocalPop(out callback);

if (callback == null)

{

for (ThreadPoolWorkQueue.QueueSegment comparand = this.queueTail; !comparand.TryDequeue(out callback) && comparand.Next != null && comparand.IsUsedUp(); comparand = this.queueTail)

Interlocked.CompareExchange<ThreadPoolWorkQueue.QueueSegment>(ref this.queueTail, comparand.Next, comparand);

}

if (callback != null)

return;

ThreadPoolWorkQueue.WorkStealingQueue[] current = ThreadPoolWorkQueue.allThreadQueues.Current;

int num = tl.random.Next(current.Length);

for (int length = current.Length; length > 0; --length)

{

ThreadPoolWorkQueue.WorkStealingQueue workStealingQueue2 = Volatile.Read<ThreadPoolWorkQueue.WorkStealingQueue>(ref current[num % current.Length]);

if (workStealingQueue2 != null && workStealingQueue2 != workStealingQueue1 && workStealingQueue2.TrySteal(out callback, ref missedSteal))

break;

++num;

}

}

因为我们把callback添加到了global work queue,所以local work queue(workStealingQueue.LocalPop(out callback))找不到callback,local work queue查找callback会在task里讲解。接着又去global work queue查找,先从global work queue的起始位置查找直至尾部,因此global work quque里的callback是FIFO的执行顺序。

public bool TryDequeue(out IThreadPoolWorkItem node)

{

int upper;

int lower;

this.GetIndexes(out upper, out lower);

while (lower != upper)

{

// ISSUE: explicit reference operation

// ISSUE: variable of a reference type

int& prevUpper = @upper;

// ISSUE: explicit reference operation

int newUpper = ^prevUpper;

// ISSUE: explicit reference operation

// ISSUE: variable of a reference type

int& prevLower = @lower;

// ISSUE: explicit reference operation

int newLower = ^prevLower + 1;

if (this.CompareExchangeIndexes(prevUpper, newUpper, prevLower, newLower))

{

SpinWait spinWait = new SpinWait();

while ((node = Volatile.Read<IThreadPoolWorkItem>(ref this.nodes[lower])) == null)

spinWait.SpinOnce();

this.nodes[lower] = (IThreadPoolWorkItem) null;

return true;

}

}

node = (IThreadPoolWorkItem) null;

return false;

}

使用自旋锁和内存读屏障来避免内核态和用户态的切换,提高了获取callback的性能。如果还是没有callback,那么就从所有的local work queue里随机选取一个,然后在该local work queue里“偷取”一个任务(callback)。
拿到callback后执行callback.ExecuteWorkItem(),通知完成。

总结
ThreadPool提供了方法调整线程池最少活跃的线程来应对不同的并发场景。ThreadPool带有2个work queue,一个golbal一个local。执行时先从local找任务,接着去global,最后才会去随机选取一个local偷一个任务,其中global是FIFO的执行顺序。Work queue实际上是数组,使用了大量的自旋锁和内存屏障来提高性能。但是在偷取任务上,是否可以考虑得更多,随机选择一个local太随意。首先要考虑偷取的队列上必须有可执行任务;其次可以选取一个不在调度中的线程的local work queue,这样降低了自旋锁的可能性,加快了偷取的速度;最后,偷取的时候可以考虑像golang一样偷取别人queue里一半的任务,因为执行完偷到的这一个任务之后,下次该线程再次被调度到还是可能没任务可执行,还得去偷取别人的任务,这样既浪费CPU时间,又让任务在线程上分布不均匀,降低了系统吞吐量!

另外,如果禁用log和ETW trace,可以使ThreadPool的性能更进一步。

原文地址: http://www.cnblogs.com/newbier/p/6192882.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/327377.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux下安装nginx (tar解压版安装) nginx1.16.1

https://blog.csdn.net/qq_40431100/article/details/104729504 Linux下安装nginx (tar解压版安装) nginx1.16.1 Jkcc 2020-03-08 16:42:30 2241 收藏 分类专栏&#xff1a; linux 运行环境 文章标签&#xff1a; linux nginx 版权 Linux下安装nginx (tar安装) nginx1.16.…

ASP.NET Core HTTP 管道中的那些事儿

前言 马上2016年就要过去了&#xff0c;时间可是真快啊。 上次写完 Identity 系列之后&#xff0c;反响还不错&#xff0c;所以本来打算写一个 ASP.NET Core 中间件系列的&#xff0c;但是中间遇到了很多事情。首先是 NPOI 的移植工作&#xff0c;移植过后还有一些Bug需要修复&…

基本属性---Linux

基本属性 看懂文件属性 Linux系统是一种典型的多用户系统&#xff0c;不同的用户处于不同的地位&#xff0c;拥有不同的权限。为了保护系统的安全性&#xff0c;Linux系统对不同的用户访问同一文件&#xff08;包括目录文件&#xff09;的权限做了不同的规定。 在Linux中我们…

Prometheus 系统监控方案

最近一直在折腾时序类型的数据库&#xff0c;经过一段时间项目应用&#xff0c;觉得十分不错。而Prometheus又是刚刚推出不久的开源方案&#xff0c;中文资料较少&#xff0c;所以打算写一系列应用的实践过程分享一下。 Prometheus 是什么&#xff1f; Prometheus是一套开源的监…

怎样批量获取文件名,批量提取文件名 文件名读取windows 批处理文件

https://jingyan.baidu.com/article/cdddd41cb0776f53cb00e1e4.html https://jingyan.baidu.com/article/cdddd41cb0776f53cb00e1e4.html 如图&#xff0c;这个文件夹中有一些名字比较奇特的文件&#xff0c;接着我们就开始获取这些文件的文件名。 如图&#xff0c;文件在一…

大三那年在某宝8块钱买的.NET视频决定了我的职业生涯

前言 谨以此文献给那些还在大学中迷茫的莘莘学子们&#xff01; 韩愈在《师说》中提出了作为师者应该做的三件事&#xff1a;传道、授业、解惑。 1.传道&#xff1a;培养学生的道德观 2.授业&#xff1a;传授学生专业技能 3.解惑&#xff1a;解答学生内心的迷茫迷惑 曾几何时&a…

为什么说Java中只有值传递(另一种角度)

转载自 为什么说Java中只有值传递 对于初学者来说&#xff0c;要想把这个问题回答正确&#xff0c;是比较难的。在第二天整理答案的时候&#xff0c;我发现我竟然无法通过简单的语言把这个事情描述的很容易理解&#xff0c;遗憾的是&#xff0c;我也没有在网上找到哪篇文章可以…

解决: -bash: docker-compose: command not found、linux 安装 docker-compose

https://blog.csdn.net/jiangyu1013/article/details/84570872 https://blog.csdn.net/guoshaoliang789/article/details/96878731 解决&#xff1a; -bash: docker-compose: command not found、linux 安装 docker-compose 微风--轻许-- 2018-11-27 18:06:01 26470 收藏 8 …

.NET Task揭秘(一)

Task为.NET提供了基于任务的异步模式&#xff0c;它不是线程&#xff0c;它运行在线程池的线程上。本着开源的精神&#xff0c; 本文以解读基于.NET4.5 Task源码的方式来揭秘Task的实现原理。 Task的创建 Task的创建方式主要有2种&#xff1a;Task.Run 和Task.Factory.StartNew…

Linux(笔记)

开启端口时&#xff0c;宝塔面板和阿里云都要开启 简介 我们为什么要学习Linux linux诞生了这么多年&#xff0c;以前还喊着如何能取代windows系统&#xff0c;现在这个口号已经小多了&#xff0c;任何事物发展都有其局限性都有其天花板。就如同在国内再搞一个社交软件取代腾讯…

自增主键与UUID的优缺点

https://blog.csdn.net/rocling/article/details/83116950 自增主键与UUID的优缺点 rocling 2018-10-17 20:15:02 8062 收藏 8 分类专栏&#xff1a; sql java 文章标签&#xff1a; sql 版权 自增主键 自增ID是在设计表时将id字段的值设置为自增的形式&#xff0c;这样当…

Git 在团队中的最佳实践--如何正确使用Git Flow

我们已经从SVN 切换到Git很多年了&#xff0c;现在几乎所有的项目都在使用Github管理, 本篇文章讲一下为什么使用Git, 以及如何在团队中正确使用。 Git的优点 Git的优点很多&#xff0c;但是这里只列出我认为非常突出的几点。 由于是分布式&#xff0c;所有本地库包含了远程库的…

拿到阿里巴巴的实习生Offer就是这么简单

转载自 拿到阿里巴巴的实习生Offer就是这么简单 一、个人简介及面试流程简介 本人本科就读于一个非985非211的一本高校&#xff0c;研究生就读于某985高校&#xff0c;目前研究方向为云计算与大数据。实习内推部门为菜鸟网络&#xff0c;所投的岗位为后台开发Java(包含大数据…

[译] RESTful API 设计最佳实践

https://juejin.im/entry/6844903503953920007 [译] RESTful API 设计最佳实践 阅读 8779 收藏 0 2017-10-16 原文链接&#xff1a; segmentfault.com 原文&#xff1a;RESTful API Design. Best Practices in a Nutshell. 作者&#xff1a;Philipp Hauer 项目资源的URL应该…

Git(笔记)

版本控制 版本控制&#xff08;Revision control&#xff09;是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史&#xff0c;方便查看更改历史记录&#xff0c;备份以便恢复以前的版本的软件工程技术。 实现跨区域多人协同开发追踪和记载一个或者多个文件…

做到我这样,你也能拿到京东Offer

转载自 做到我这样&#xff0c;你也能拿到京东Offer 最近&#xff0c;春招已经基本接近尾声了&#xff0c;我找了几位拿到名企Offer的粉丝&#xff0c;请他们总结了面试经验&#xff0c;近期会分批的推送给大家。希望给那些正在准备秋招的同学提供些帮助。这是一篇7000字的长文…

迁移传统.net 应用到.net core [视频]

.net core是.NET技术的未来&#xff0c;这一点正在被越来越多的公司认识到&#xff0c;但是如何将传统的.NET应用迁移到.NET Core是一个迫切需要解决的问题。 对于传统.NET应用来说&#xff0c;使用和不使用.NET Core可能并不能直接给企业带来好处&#xff0c;相反使用中遇到了…

POI

Poi&#xff08;适合小数据量&#xff09; Apache POI 官网&#xff1a;https://poi.apache.org/ POI是Apache软件基金会的&#xff0c;POI为“Poor Obfuscation Implementation”的首字母缩写&#xff0c;意为“简洁版的模糊实现”。 所以POI的主要功能是可以用Java操作Micr…

因 Redis Key 命令不规范,导致熬了一个通宵才把Key删完了!

https://mp.weixin.qq.com/s/7FL0nUTk6aFmAb2J__5Mtw 因 Redis Key 命令不规范&#xff0c;导致熬了一个通宵才把Key删完了&#xff01; 点击关注 &#x1f449; Java面试那些事儿 9月3日 # 前言 由于有一条业务线不理想&#xff0c;高层决定下架业务。对于我们技术团队而言&a…

使用 CefSharp 在 C# App 中嵌入 Chrome 浏览器

介绍 以前曾试过在app中整合一个可靠又快速的web浏览器吗&#xff1f; 在本文中&#xff0c;你会学到如何轻松地将奇妙的CefSharp网页浏览器组件&#xff08;基于Chromium&#xff09;集成到你的C# app中。 然后&#xff0c;你可以使用此web浏览器&#xff1a; 给用户提供一个集…