C# 浅谈ThreadPool -- 上篇(Enqueue)
来源:互联网 发布:福利彩票预测软件 编辑:程序博客网 时间:2024/05/21 06:41
这一切是为啥
TPL 都出来好几年了,每天都在用。但是我从来没关心过到底 Task.Run背后到底发生了啥。
有了await async 这等方便快捷的语法糖之后更是连直接用Task都少了。
Thread 这个知识面对我而言一直和黑洞一样,反正就这么写这么写然后这么写,就好使了,至于为啥,我也不知道。
一切始于有一天和同事的一段对话:
同事:一个 App Pool 有几个 Thread Pool ?
我:一个。
同事:那如果一个 App Pool 里面有好几个Application呢?
我: 应该还是一个 Thread Pool。
同事: Thread Pool如何做到既保证各个App Domain之间相互隔离,又能使线程共享呢?
我:。。。你给我几天,我得去看源代码
猛一看,吓一跳
TPL是一个很长很长的话题,这一篇我打算只将重点放在 ThreadPool 和 ThreadPoolWorkQueue 上,说一说我对一个Task是如何被塞进
Queue 里排队,又如何被拿出来执行的个人理解。
我知道有很多国内外的大神都做过ThreadPool的源代码分析,就当我这个版本是交作业吧。
先来一个缩略图:(我的作图能力实在是不怎么样,现在看来图里面的汽车真是让人一秒出戏)
总的来说一个Task从开始到执行会经历Enqueue (进队)和 Dequeue(出队)这两个大步骤。
这俩步骤是谁协调起来的呢?是托管代码ThreadPool 以及非托管代码 ThreadpoolMgr.
当然这只是一个概括的说法,实际上这个pipiline很长,进队和出队的过程也很复杂,底下我会慢慢讲来。
几个概念
1. ThreadPool托管(ThreadPool.cs):
主要用于管理Queue和线程。Task一个一个个放进Queue里面排队,然后找非托管ThreadpoolMrg 索要线程,有合适的线程就把Queue里的Task拿出来执行,一切都是为了实现:异步。
TheadPool是一个Static的类,也就是说每个App Domain都会有一个ThreadPool实例,并且是相互隔离的。
[HostProtection(Synchronization=true, ExternalThreading=true)] public static class ThreadPool {
......
2. ThreadPoolWorkQueue(线程池队列):
传说中的Queue的具体实现, 每个Threadpool.cs 的实例有一个 ThreadPoolGlobals实例,它也是static的类,里面包含了一个ThreadPoolWorkQueue实例,也就是说每个App Domain都会有一个ThreadPoolWoekQueue,并且是相互隔离的。
internal static class ThreadPoolGlobals { ......
3. 公用队列和私有队列
ThreadPoolWorkQueue实例又包含两种队列,一个公用的,一堆私用的。
公用的public Queue:
App Domain里面所有线程都可以使用, 大家好才是真的好。
私有的WorkStealingQueue:
每个线程都会有一个ThreadPoolWorkQueueThreadLocals实例,里面包含了一个WorkStealingQueue实例。通过这个类的ThreadSatic Attribute 可以得知,这个类的实例是每个线程一个。 每当有新的线程加入工作,一个新的ThreadPoolWorkQueueThreadLocals实例就会被创建,并且加入到ThreadPoolWoekQueue实例的allThreadQueues里面。从私有队列的名字就能看出来,一个线程可以去偷其他线程的私有队列里面的Task.
internal sealed class ThreadPoolWorkQueueThreadLocals { [ThreadStatic] [SecurityCritical] public static ThreadPoolWorkQueueThreadLocals threadLocals; public readonly ThreadPoolWorkQueue workQueue; public readonly ThreadPoolWorkQueue.WorkStealingQueue workStealingQueue;
那么,来看一下Enqueue的过程吧
源代码来自.NET Framework 4.7.1 的官方网站点击打开链接(http://referencesource.microsoft.com)
在这里我将快速跳过ExecutionContext的部分和ThreadPoolTaskScheduler的部分,这两段的内容太大,我打算单独开两篇来写些自己的体会。
先从最基础的看起,不要Generic,不管TaskScheduler, 只是简简单单的Task.Run(Action).
这个方法调用Task.InternalStartNew 方法。 值得一提的是,TaskFactory.StartNew方法同样也调用Task.InternalStartNew,两者的区别是传入方法的变量不同。个人认为如果使用TaskFactory.StartNew则需要对TaskCreationOption 以及TaskScheduler.Default 和 TaskScheduler.Current的区别有一定的了解,不然还是老老实实的用Task.Run比较好。
[MethodImplAttribute(MethodImplOptions.NoInlining)] public static Task Run(Action action) { StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; return Task.InternalStartNew(null, action, null, default(CancellationToken), TaskScheduler.Default, TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None, ref stackMark); }
进入InternalStartNew之后,最重要的两步为:PossiblyCaptureContext以及ScheduleAndStart.
若要看懂PossiblyCaptureContext的源代码首先需要了解什么是ExecutionContext.这里只总结概括为:
因为传入这个Task的Action之后(也许)会被别的线程执行,则需要抓取当前线程一系列的Context,方便传给之后的执行线程。这个过程叫做Context Flow.
ScheduleAndStart 这个方法的名字足以说明一切, 首先把Task塞进队伍里排队(Schedule), 然后试图索要空闲线程来执行它(Start).
internal static Task InternalStartNew( Task creatingTask, Delegate action, object state, CancellationToken cancellationToken, TaskScheduler scheduler, TaskCreationOptions options, InternalTaskOptions internalOptions, ref StackCrawlMark stackMark) { if (scheduler == null) { throw new ArgumentNullException("scheduler"); } Contract.EndContractBlock(); Task t = new Task(action, state, creatingTask, cancellationToken, options, internalOptions |
InternalTaskOptions.QueuedByRuntime, scheduler); t.PossiblyCaptureContext(ref stackMark); t.ScheduleAndStart(false); return t; }
下图的m_taskScheduler是什么呢?对于Task.Run来说,默认为TaskScheduler.Default, 也就是ThreadPoolTaskScheduler的实例,从这个实例我们将进入ThreadPool。
[SecuritySafeCritical] internal void ScheduleAndStart(bool needsProtection) { //.....省略不重要部分 try {m_taskScheduler.InternalQueueTask(this); } catch (ThreadAbortException tae) //.....省略不重要部分
ThreadPoolTaskScheduler 的父类是TaskScheduler,接上文从InternalQueueTask跳转到ThreadPoolTaskScheduler 类的QueueTask方法。
[SecurityCritical] internal void InternalQueueTask(Task task) { Contract.Requires(task != null); task.FireTaskScheduledIfNeeded(this); this.QueueTask(task); }
这里可以看到如果是LongRunning的Task则不会进入TaskPool的部分,而是返璞归真直接创建新的background线程。
如果不需要LongRunning的Task则调用ThreadPool 的UnsafeQueueCustomWorkItem方法。前面说到过ThreadPool是一个static class, 则每个App Domain有一个托管的实例。注意在这里我们传入整个Task实例。
[SecurityCritical] protected internal override void QueueTask(Task task) { if ((task.Options & TaskCreationOptions.LongRunning) != 0) { Thread thread = new Thread(s_longRunningThreadWork); thread.IsBackground = true; thread.Start(task); } else { bool forceToGlobalQueue = ((task.Options & TaskCreationOptions.PreferFairness) != 0); ThreadPool.UnsafeQueueCustomWorkItem(task, forceToGlobalQueue); } }
首先,EnsureVMInitialized方法会进入非托管的类ThreadpoolMrg 初始化ThreadPool. 关于这个类我们下一篇会单独讲。
然后
[SecurityCritical] internal static void UnsafeQueueCustomWorkItem(IThreadPoolWorkItem workItem, bool forceGlobal) { Contract.Assert(null != workItem); EnsureVMInitialized(); try { } finally { ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal); } }
前文讲过ThreadPoolGlobals也是一个静态类,每个App Domain 都会有一个实例。 我们来看一下它的代码:
每个ThreadPoolGlobals实例里都有一个ThreadPoolWorkQueue实例,它就是我们所说的队列。
internal static class ThreadPoolGlobals { //Per-appDomain quantum (in ms) for which the thread keeps processing //requests in the current domain. public static uint tpQuantum = 30U; public static int processorCount = Environment.ProcessorCount; public static bool tpHosted = ThreadPool.IsThreadPoolHosted(); public static volatile bool vmTpInitialized; public static bool enableWorkerTracking; [SecurityCritical] public static ThreadPoolWorkQueue workQueue = new ThreadPoolWorkQueue(); [System.Security.SecuritySafeCritical] // static constructors should be safe to call static ThreadPoolGlobals() { } }
ThreadPoolWorkQueue 是一个Wrapper 类, 里面包含了一个公有的队列, 和一个线程私有队列数组。 顾名思义,每个线程都有一个这样的私有队列,而为了负载平衡,每个队列可以去别的线程私有队列里“”偷“” Task.
internal volatile QueueSegment queueHead; internal volatile QueueSegment queueTail; internal bool loggingEnabled; internal static SparseArray<WorkStealingQueue> allThreadQueues = new SparseArray<WorkStealingQueue>(16); // private volatile int numOutstandingThreadRequests = 0; public ThreadPoolWorkQueue() { queueTail = queueHead = new QueueSegment(); loggingEnabled = FrameworkEventSource.Log.IsEnabled(EventLevel.Verbose, FrameworkEventSource.Keywords.ThreadPool|FrameworkEventSource.Keywords.ThreadTransfer); }
这个线程私有队列数组是一个internal class, 并没有什么特殊性,只是当容量不够需要扩容时,以两倍数增长。
internal class SparseArray<T> where T : class { private volatile T[] m_array; internal SparseArray(int initialSize) { m_array = new T[initialSize]; } internal T[] Current { get { return m_array; } } internal int Add(T e) { while (true) { T[] array = m_array; lock (array) { for (int i = 0; i < array.Length; i++) { if (array[i] == null) { Volatile.Write(ref array[i], e); return i; } else if (i == array.Length - 1) { // Must resize. If we raced and lost, we start over again. if (array != m_array) continue; T[] newArray = new T[array.Length * 2]; Array.Copy(array, newArray, i + 1); newArray[i + 1] = e; m_array = newArray; return i + 1;
回到上文的进队列入口代码:
ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal);
其实非常简单,可以概括为两步:
当前线程有没有属于自己的私有队列呢?如果有,则把Task塞入这个私有队列。
如果没有,则塞入公用队列。
在这里并不会为当前线程创建私有队列,如果没有,就算了,直接放入公用队列,反正每个线程都可以访问公用队列。
那么什么时候会创建线程的私有队列呢?答案是,Dequeue(出队的时候)。
然而出队的时候,并不一定还是当前线程来执行Task, 这时候就会体现“”偷“”字的重要性。 自己没有,可以偷别的线程的队列中的Task. 这也是出队过程的总体概括:自己的队列有需要执行的Task吗? 公用队列有吗? 别人的有吗?
而最重要的一步,就是最后一行代码:EnsureThreadRequested, 用大白话说来就是,我刚塞进去一个Task, 我要线程!
[SecurityCritical] public void Enqueue(IThreadPoolWorkItem callback, bool forceGlobal) { ThreadPoolWorkQueueThreadLocals tl = null; if (!forceGlobal) tl = ThreadPoolWorkQueueThreadLocals.threadLocals; if (loggingEnabled) System.Diagnostics.Tracing.FrameworkEventSource.Log.ThreadPoolEnqueueWorkObject(callback); if (null != tl) { tl.workStealingQueue.LocalPush(callback); } else { QueueSegment head = queueHead; while (!head.TryEnqueue(callback)) { Interlocked.CompareExchange(ref head.Next, new QueueSegment(), null); while (head.Next != null) { Interlocked.CompareExchange(ref queueHead, head.Next, head); head = queueHead; } } } EnsureThreadRequested(); }
EnsureThreadRequested 会进入ThreadPool的RequestWorkerThread方法。 很多人一看到extern关键字就不想再继续看下去了,感觉已经出了需要了解的范围。
[System.Security.SecurityCritical] // auto-generated [ResourceExposure(ResourceScope.None)] [DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)] [SuppressUnmanagedCodeSecurity] internal static extern bool RequestWorkerThread();
幸好现在corlib也是开源的了,Github上就有,我才得以对非托管的ThreadPool有一点点的了解,下面一篇会重点讲讲ThreadPool非托管的部分。
题外话:
双十一什么都没有买耶,本来京东搞了个海外包邮活动,又取消了,生意做成这样真是打脸啊。
晚安
Fi
- C# 浅谈ThreadPool -- 上篇(Enqueue)
- C# 浅谈ThreadPool -- 中篇(ThreadPoolMrg)
- C# ThreadPool
- C# ThreadPool
- 浅谈ThreadPool 线程池
- 浅谈ThreadPool 线程池
- 浅谈ThreadPool 线程池
- 浅谈ThreadPool 线程池
- 浅谈ThreadPool 线程池
- 浅谈ThreadPool 线程池
- C# 中的 ThreadPool
- C# ThreadPool类简介
- C# ThreadPool学习笔记
- C# ThreadPool 的改进
- c# ThreadPool 应用实例
- C#线程池ThreadPool
- C# ThreadPool 线程同步
- C# ThreadPool理解
- mysql5.7 主从配置
- Android 之路4---Java简介
- C++读取BMP位图数据的方法
- 树状数组习题:棋子等级
- Android定时广播和定时服务两种实现方式
- C# 浅谈ThreadPool -- 上篇(Enqueue)
- Linux Git常见命令
- 百度地图Android,v4_5_0.so
- 数据挖掘中常用的数据清洗方法有哪些?
- hahahaha
- Matlab 之 im2col 函数用法
- 桶排序java
- 【DevExpress v17.2新功能预告】DevExpress ASP.NET Scheduler新的自适应功能
- 相机成像原理(四个坐标系转换)