1. 程式人生 > >再看C# ThreadPool與Task的認識總結

再看C# ThreadPool與Task的認識總結

def red hidden 可執行 隊列 process 線程池 increase 直接

工作線程與I/O線程
在ThreadPool中有這樣一個方法:

public static bool SetMaxThreads(int workerThreads, int completionPortThreads);

此方法中有兩個參數:workerThreads和completionPortThreads。這兩個參數引申出了兩個概念:輔助線程(也叫工作線程)和異步 I/O 線程。這兩個線程有什麽區別麽?通過查閱資料,我們可以了解到,工作線程其實就是我們編碼主動向ThreadPool中創建的線程。而I/O線程是線程池中預先保留出來的部分線程,這部分線程的作用是為了分發從IOCP(I/O completion port) 中的回調。

那麽什麽是IOCP回調呢?

在CLR內部,系統維護了一個IOCP(I/O completion port),它提供了處理多個異步I/O請求的線程模型。我們可以把IOCP看做是一個消息隊列。當一個進程創建了一個IOCP,即創建了一個隊列。當異步I/O請 求完成時,設備驅動程序就會生成一個I/O完成包,將它按照FIFO方式排隊列入該完成端口。之後,會由I/O線程提取完成I/O請求包,並調用之前的委托。註意:異步調用服務時,回調函數都是運行於CLR線程池的I/O線程當中

I/O線程是由CLR調用的,通常情況下,我們不會直接用到它 。但是線程池中區分它們的目的是為了避免線程都去處理I/O回調而被耗盡,從而引發死鎖。在編程時,開發人員需要關註的是確保I/O線程返回到線程池,I/O回調代碼應該做盡量小的工作,並盡快返回到線程池,否則I/O線程會很快消耗光。如果回調代碼中的工作很多的話,應該考慮把工作拆分到一個工作者線程中去。否則,I/O線程被耗盡,大量工作線程空閑,可能導致死鎖。

再補充一下,當執行I/O操作的時候,無論是同步I/O操作還是異步I/O操作,都會調用的Windows的API方法,比如,當讀取文件時,調用ReadFile函數。該方法會將你的當前線程從用戶態轉變成內核態,會生成一個I/O請求包,並初始化這個請求包。ReadFile會向內核傳遞,根據這個請求包,windows內核知道需要將這個I/O操作發送給哪個硬件設備。這些I/O操作會進入設備自己的處理隊列中,該隊列由這個設備的驅動程序維護。

如果此時是同步I/O操作,那麽在硬件設備操作I/O的時候,發出I/O請求的線程由於無事可做被windows變成睡眠狀態,當硬件設備完成操作後,再喚醒這個線程。這種方式非常直接,但是性能不高,如果請求數很多,那麽休眠的線程數也很多,浪費了大量資源。

如果是異步I/O操作(.Net中,異步的I/O操作大部分為BeginXXX的形式 ),該方法在Windows把I/O請求包發送到設備的處理隊列後就返回。同時,在調用異步I/O操作時,即調用BeginXXX方法的時候,需要傳入一個委托,該委托方法會隨著I/O請求包一路傳遞到設備的驅動程序。在設備處理完I/O請求包後,將該委托再放到CLR線程池隊列。

總結來說,IOCP(I/O completion port)中有2個隊列,一個是先進先出的隊列,存放的是IO完成包,即已經完成的IO操作需要執行回調方法。還有一個隊列是線程隊列,IOCP會預分配一些線程在這個隊列中,這樣會比即時創建線程處理I/O請求速度更快。這個隊列是後進先出的,好處是下一個請求的到來可能還是用之前的線程來處理,就不需要進行線程上下文切換,提高了性能。

這裏有一個IOCP的解釋,寫的很好。http://gamebabyrocksun.blog.163.com/blog/static/57153463201036104134250/

Task的運行原理分析

Task與ThreadPool什麽關系呢?簡單來說,Task是基於ThreadPool實現的,當然被標記為LongRunning的Task(單獨創建線程實現)除外。Task被創建後,通過TaskScheduler執行工作項的分配。TaskScheduler會把工作項存儲到兩類隊列中: 全局隊列與本地隊列。全局隊列被設計為FIFO的隊列。本地隊列存儲在線程中,被設計為LIFO.

當主程序創建了一個Task後,由於創建這個Task的線程不是線程池中的線程,則TaskScheduler 會把該Task放入全局隊列中。

如果這個Task是由線程池中的線程創建,並且未設置TaskCreationOptions.PreferFairness標記(默認情況下未設置),TaskScheduler 會把該Task放入到該線程的本地隊列中。如果設置了TaskCreationOptions.PreferFairness標記,則放入全局隊列。

官方的解釋是: 最高級任務(即不在其他任務的上下文中創建的任務)與任何其他工作項一樣放在全局隊列上。 但是,嵌套任務或子任務(在其他任務的上下文中創建)的處理方式大不相同。 子任務或嵌套任務放置在特定於執行父任務的線程的本地隊列上。 父任務可能是最高級任務,也可能是其他任務的子任務。

那麽任務放入到兩類隊列中後,是如何被執行的呢?

當線程池中的線程準備好執行更多工作時,首先查看本地隊列。 如果工作項在此處等待,直接通過LIFO的模式獲取執行。 如果沒有,則向全局隊列以FIFO的模式獲取工作項。如果全局隊列也沒有工作項,則查看其他線程的本地隊列是否有可執行工作項,如果存在可執行工作項,則以FIFO的模式出隊執行。

There is one thread pool per process. Beginning with the .NET Framework 4, the default size of the thread pool for a process depends on several factors, such as the size of the virtual address space. A process can call the GetMaxThreads method to determine the number of threads. The number of threads in the thread pool can be changed by using the SetMaxThreads method. Each thread uses the default stack size and runs at the default priority.

Note

Unmanaged code that hosts the .NET Framework can change the size of the thread pool by using the CorSetMaxThreads function, defined in the mscoree.h file.

The thread pool provides new worker threads or I/O completion threads on demand until it reaches the minimum for each category. When a minimum is reached, the thread pool can create additional threads in that category or wait until some tasks complete. Beginning with the .NET Framework 4, the thread pool creates and destroys worker threads in order to optimize throughput, which is defined as the number of tasks that complete per unit of time. Too few threads might not make optimal use of available resources, whereas too many threads could increase resource contention.

Note

When demand is low, the actual number of thread pool threads can fall below the minimum values.

You can use the GetMinThreads method to obtain these minimum values.

資料參考:

https://www.cnblogs.com/vveiliang/p/7943003.html

https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadpool?redirectedfrom=MSDN&view=netframework-4.7.2

再看C# ThreadPool與Task的認識總結