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 会进入ThreadPoolRequestWorkerThread方法。 很多人一看到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