.NET編程之線程池內幕

來源:互聯網
上載者:User
本文通過對.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;elsethreadPoolWorkQueue.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 typeint& prevUpper = @upper;// ISSUE: explicit reference operationint newUpper = ^prevUpper;// ISSUE: explicit reference operation// ISSUE: variable of a reference typeint& prevLower = @lower;// ISSUE: explicit reference operationint 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的效能更進一步。

以上就是.NET編程之線程池內幕的內容,更多相關內容請關注topic.alibabacloud.com(www.php.cn)!

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.