Overview
如今的應用程序越來越復雜,我們常常需要使用《異步編程:線程概述及使用》中提到的多線程技術來提高應用程序的響應速度。這時我們頻繁的創建和銷毀線程來讓應用程序快速響應操作,這頻繁的創建和銷毀無疑會降低應用程序性能,我們可以引入緩存機制解決這個問題,此緩存機制需要解決如:緩存的大小問題、排隊執行任務、調度空閑線程、按需創建新線程及銷毀多余空閑線程……如今微軟已經為我們提供了現成的緩存機制:線程池
1、.NET框架為每一個進程提供了一個線程池,每當您啟動線程時,都會花費幾百微秒來組織諸如新的私有局部變量堆棧之類的東西。
2、只有全局一個隊列和n本地線程任務隊列,無法取消任務,無法限制任務執行速度等等
3、當一個等待操作完成時,線程池中的一個輔助線程就會執行對應的回調函數
4、線程池中的線程由系統進行管理,程序員不需要費力於線程管理,可以集中精力處理應用程序任務。
5、線程池線程都是后台線程。每個線程都使用默認堆棧大小1MB,以默認的優先級運行,並處於多線程單元中,您可以隨意更改池線程的優先級— 當釋放回池時,它將恢復正常。線程池通過共享和回收線程來減少這些開銷,從而允許在非常精細的級別應用多線程,而不會降低性能。
6、如果所有線程池線程都始終保持繁忙,但隊列中包含掛起的工作,則線程池將在一段時間之后創建另一個輔助線程。
7、但線程的數目永遠不會超過最大值。超過最大值的其他線程可以排隊,但它們要等到其他線程完成后才啟動。
8、如果某個線程在托管代碼中空閑(如正在等待某個事件),則線程池將插入另一個輔助線程來使所有處理器保持繁忙。
9、一種是用於處理CPU密集型邏輯的工作線程,即它主要使用CPU來執行其邏輯,如任何計算等,另一種是I / O線程,這些線程用於執行I / O請求或耗時的請求,例如讀/寫到文件系統, 調用數據庫或調用任何第三方 API/請求。
10、線程池不保證全局隊列工作項的處理順序
11、如果工作項完成的時間太長(具體多長沒有正式公布)線程池會創建更多的工作者線程,如果工作像完成速度開始變快,工作者線程會被銷毀
線程池從其池中的一個線程開始。分配任務時,池管理器會"注入"新線程以處理額外的並發工作負載,最高可達最大限制。在足夠長的不活動時間后,如果池管理器懷疑這樣做會帶來更好的吞吐量,則池管理器可能會"停用"線程。
您可以通過調用 來設置池將創建的線程的上限;默認值為:ThreadPool.SetMaxThreads
12、如果全局隊列也為空,工作者線程會進入睡眠狀態等待事情的發生,如果睡眠了太長時間,他會自己醒來並銷毀至深允許系統回收線程使用的資源
13、最好是將線程池看成一個黑盒,不要拿單個應用程序去衡量它的性能,因為它不是針對某個單獨應用程序而設計的。線程池內部會更改它的管理線程的方式,所以大多應用程序的性能會變的越來越好
1、線程池的工作原理
線程池的全局隊列(global Queue)
當調用ThreadPool.QueueUserWorkItem()添加工作項時,該工作項會被添加到線程池的全局隊列中。線程池中的空閑線程以FIFO的順序將工作項從全局隊列中取出並執行,但並不能保證按某個指定的順序完成。
線程的全局隊列是共享資源,所以內部會實現一個鎖機制。當一個任務內部會創建很多子任務時,並且這些子任務完成得非常快,就會造成頻繁的進入全局隊列和移出全局隊列,從而降低應用程序的性能。基於此原因,線程池引擎為每個線程引入了局部隊列。
線程的局部隊列(local Queue)
為我們帶來兩個性能優勢:任務內聯化(task inlining)和工作竊取機制。
1) 任務內聯化(task inlining)----活用頂層任務工作線程

static void Main(string[] args) { Task headTask= new Task(() => { DoSomeWork(null); }); headTask.Start(); Console.Read(); } private static void DoSomeWork(object obj) { Console.WriteLine("任務headTask運行在線程“{0}”上", Thread.CurrentThread.ManagedThreadId); var taskTop = new Task(() => { Thread.Sleep(500); Console.WriteLine("任務taskTop運行在線程“{0}”上", Thread.CurrentThread.ManagedThreadId); }); var taskCenter = new Task(() => { Thread.Sleep(500); Console.WriteLine("任務taskCenter運行在線程“{0}”上", Thread.CurrentThread.ManagedThreadId); }); var taskBottom = new Task(() => { Thread.Sleep(500); Console.WriteLine("任務taskBottom運行在線程“{0}”上", Thread.CurrentThread.ManagedThreadId); }); taskTop.Start(); taskCenter.Start(); taskBottom.Start(); Task.WaitAll(new Task[] { taskTop, taskCenter, taskBottom }); }
分析:(目前內聯機制只有出現在等待任務場景)
這個示例,我們從Main方法主線程中創建了一個headTask頂層任務並開啟。在headTask任務中又創建了三個嵌套任務並最后WaitAll() 這三個嵌套任務執行完成(嵌套任務安排在局部隊列)。此時出現的情況就是headTask任務的線程被阻塞,而“任務內聯化”技術會使用阻塞的headTask的線程去執行局部隊列中的任務。因為減少了對額外線程需求,從而提升了程序性能。
局部隊列“通常”以LIFO的順序抽取任務並執行,而不是像全局隊列那樣使用FIFO順序。LIFO順序通常用有利於數據局部性,能夠在犧牲一些公平性的情況下提升性能。
數據局部性的意思是:運行最后一個到達的任務所需的數據都還在任何一個級別的CPU高速緩存中可用。由於數據在高速緩存中任然是“熱的”,因此立即執行最后一個任務可能會獲得性能提升。
2) 工作竊取機制----活用空閑工作線程
當一個工作線程的局部隊列中有很多工作項正在等待時,而存在一些線程卻保持空閑,這樣會導致CPU資源的浪費。此時任務調度器(TaskScheduler)會讓空閑的工作線程進入忙碌線程的局部隊列中竊取一個等待的任務,並且執行這個任務。
由於局部隊列為我們帶來了性能提升,所以,我們應盡可能地使用TPL提供的服務(任務調度器(TaskScheduler)),而不是直接使用ThreadPool的方法。
我們用一個示例來說明:
1、.net 會為每個進程生成一個線程池。線程池的初始值線程數是 cpu邏輯內核數。后面連續建線程要每間隔500ms新建一個線程。即使突然並發大量任務也是按這個進度進來線程。
2、可以通過設置min thread(默認等於cpu內核數) 提高 線程池一開始的線程數,從而提高並發數。 后面連續建線程要每間隔500ms新建一個線程。
3、本地隊列:線程池中的每一個線程都會綁定一個 ThreadPoolWorkQueueThreadLocals 實例,在 workStealingQueue 這個字段上保存着本地隊列。
4、全局隊列:每個進程只有一個全局隊列, 是由 ThreadPoolWorkQueue 維護的,同時它也是整個隊列系統的入口,直接被 ThreadPool 所引用。
5、在線程池線程A中生成的任務,會安排入當前線程A的任務隊列。
6、當前線程池的線程A的任務隊列已經執行完成,如果全局任務隊列也沒有任務了,就會去查看其他線程B的任務隊列,如果其他線程B的任務隊列還有很對沒完成的,當前線程A就會偷它B的任務執行。
7、線程池使用IO模型有兩種:IOCP I/O模型和verlapped I/O模型。
8、線程池將自己的線程划分工作者線程(輔助線程)和I/O線程。前者用於執行普通的操作,后者專用於異步IO,比如文件和網絡請求,注意,分類並不說明兩種線程本身有差別,內部依然是一樣的。
2 線程池的應用范圍
在以下幾種情況下,適合於使用線程池線程:
(1)不需要前台執行的線程。
(2)不需要在使用線程具有特定的優先級。
(3)線程的執行時間不易過長,否則會使線程阻塞。由於線程池具有最大線程數限制,因此大量阻塞的線程池線程可能會阻止任務啟動。
(4)不需要將線程放入單線程單元。所有 ThreadPool 線程均不處於多線程單元中。
(5)不需要具有與線程關聯的穩定標識,或使某一線程專用於某一任務。
(6)一種是在應用程序中,線程把大部分的時間花費在等待狀態,等待某個事件發生,然后才能給予響應,這一般使用ThreadPool(線程池)來解決
(7)一種情況是在線程平時都處於休眠狀態,只是周期性地被喚醒,這一般使用Timer(定時器)來解決。下面對ThreadPool類進行詳細說明。
3、有多種方法可以進入線程池:
- 通過任務並行庫(來自框架 4.0)
- 通過呼叫
ThreadPool.QueueUserWorkItem
- 通過異步委托
- 通過
BackgroundWorker
4、以下構造間接使用線程池:
- WCF、遠程處理、ASP.NET 和 ASMX Web 服務應用程序服務器
System.Timers.Timer
和System.Threading.Timer
- 以異步結尾的框架方法,例如 on(基於事件的異步模式)和大多數XXX方法(異步編程模型模式)
WebClient
Begin
- 普林克
5、優化線程池
線程池從其池中的一個線程開始。分配任務時,池管理器會"注入"新線程以處理額外的並發工作負載,最高可達最大限制。在足夠長的不活動時間后,如果池管理器懷疑這樣做會帶來更好的吞吐量,則池管理器可能會"停用"線程。
可以通過調用 來設置池將創建的線程的上限;默認值為:workerThreads: 默認大小邏輯cpucore數, completionPortThreads: 默認大小邏輯。
max threads 初始值:32位平台 1023,64位平台 short.MaxValue32767。
32位環境中的 workerThreads:1023 默認大小邏輯cpucore數, completionPortThreads:25 默認大小邏輯cpucore數
64位環境中的 workerThreads:32767 默認大小邏輯cpucore數, completionPortThreads:1000 默認大小邏輯cpucore數
默認數字是由進程的關聯掩(affinity mask)碼決定的。
min threads 初始值:運行環境 CPU 核心數,可通過 ThreadPool.SetMinThreads
進行設置,參數有效范圍是 [1, max threads]。(這些數字可能因硬件和操作系統而異。之所以存在許多線程,是為了確保某些線程被阻止時取得進展(在等待某些條件時空轉,例如來自遠程計算機的響應)。
您還可以通過調用 來設置下限。下限的作用更微妙:它是一種高級優化技術,指示池管理器在達到下限之前不要延遲線程的分配。當存在阻塞的線程時,提高最小線程計數可提高並發性(請參閱側邊欄)。ThreadPool.SetMinThreads
CompletionPort的知識點:https://www.cnblogs.com/cdaniu/p/15782960.html
6、排隊工作項
1、通過調用 ThreadPool.QueueUserWorkItem 並傳遞 WaitCallback 委托來使用線程池。
2、通過使用 ThreadPool.RegisterWaitForSingleObject 並傳遞 WaitHandle(在向其發出信號或超時時,它將引發對由 WaitOrTimerCallback 委托包裝的方法的調用)來將與等待操作相關的工作項排隊到線程池中。
若要取消等待操作(即不再執行WaitOrTimerCallback委托),可調用RegisterWaitForSingleObject()方法返回的RegisteredWaitHandle的 Unregister 方法。
3、如果您知道調用方的堆棧與在排隊任務執行期間執行的所有安全檢查不相關,則還可以使用不安全的方法 ThreadPool.UnsafeQueueUserWorkItem 和 ThreadPool.UnsafeRegisterWaitForSingleObject。UnsafeQueueUserWorkItem 和 RegisterWaitForSingleObject都會捕獲調用方的堆棧,此堆棧將在線程池線程開始執行任務時合並到線程池線程的堆棧中。如果需要進行安全檢查,則必須檢查整個堆棧,但它還具有一定的性能開銷。使用“不安全的”方法調用並不會提供絕對的安全,但它會提供更好的性能。
7、方法的是使用
由於飢餓和死鎖的問題存在,所以不建議使用GetMaxThreads、SetMaxThreads、GetMinThreads、SetMinThreads、GetAvailableThreads--CLR Via C#
ThreadPool.QueueUserWorkItem 用法
int WorkThreadCount, CompletionThreadcCont; ThreadPool.GetMaxThreads(out WorkThreadCount, out CompletionThreadcCont); ThreadPool.SetMinThreads(1001, 10); ThreadPool.GetMinThreads(out WorkThreadCount, out CompletionThreadcCont);// 默認最小值是cpu個數 ThreadPool.QueueUserWorkItem(new WaitCallback(count), null); Console.WriteLine(ThreadPool.PendingWorkItemCount);//當前排隊等候處理的工作項的數目。 Console.WriteLine(ThreadPool.CompletedWorkItemCount);//到目前為止已處理的工作項的數目。 Console.WriteLine(ThreadPool.ThreadCount);//獲取當前程序的 線程池的線程數 ThreadPool.GetAvailableThreads(out WorkThreadCount, out CompletionThreadcCont);//獲取剩余的可用線程 count(null); void count(object? obg) { Console.WriteLine($"WorkThreadCount:{WorkThreadCount},CompeTionThreadcCont:{CompletionThreadcCont}"); } //將方法委托給線程池 ThreadPool.QueueUserWorkItem( o => { int i; i = Convert.ToInt32(o); i++; Console.WriteLine("sfdsdf"); },1 );
RegisteredWaitHandle RegisterWaitForSingleObject(WaitHandle, WaitOrTimerCallback, Object, TimeSpan, Boolean) 方法是定時器,定時執行某個函數
WaitHandle waitObject: 等待完成句柄 AutoSetWaitHandle、 ManualSetWaitHandle
WaitOrTimerCallback:回調函數
Object:委托參數
TimeSpan timeout:並指定一個 TimeSpan 值來表示超時時間。時間到了執行WaitOrTimerCallback函數。
Boolean executeOnlyOnce:是否只執行一次委托。如果為 true,表示在調用了委托后,線程將不再在 waitObject 參數上等待;如果為 false,表示每次完成等待操作后都重置計時器,直到等到信號(set())。
返回值:RegisteredWaitHandle,用來注銷等待句柄
案例
Random random = new Random(); AutoResetEvent wh = new AutoResetEvent(false);//將初始狀態設置為非終止 RegisteredWaitHandle RWH =ThreadPool.RegisterWaitForSingleObject(wh, writeangen, null, 2000, false);//2s true=執行一次結束 void writeangen(object? state, bool timedOut) { Console.WriteLine("Timeout ");//定時輸出 } Console.ReadLine(); wh.Set(); RWH.Unregister(wh);//注銷等待句柄,應為