ThreadPool(線程池)介紹

>>返回《C# 併發編程》html

1. 線程池的由來

1.1. 線程池出現前

解決三個需求編程

  1. 異步調用方法
  2. 按時間間隔調用方法
  3. 當一個內核對象收到信號時調用方法

開發人員常常建立一個新線程來執行單個任務,當任務完成時,該線程就會死亡。多線程

  • 與進程相比,建立和銷燬線程速度更快,而且佔用的OS資源更少,可是建立銷燬線程確定不是免費的
  • 建立線程過程
    1. 分配和初始化內核對象,分配和初始化線程的堆棧內存
    2. Windows 向進程中的每一個DLL發送 DLL_THREAD_ATTACH 通知,從而致使磁盤中的pages被轉移到內存中以便代碼能夠執行
  • 銷燬線程過程
    • 當線程死亡時,將向每一個DLL發送 DLL_THREAD_DETACH 通知,該線程的堆棧內存被釋放,而且內核對象被釋放(若是其使用計數變爲0
  • 所以,建立和銷燬線程有不少開銷,這些開銷與建立線程最初要執行的工做無關

1.2. 線程池的誕生

  • Microsoft實現了一個線程池,該線程池首次在 Windows2000 中得到支持。
  • 當 .NET Framework 團隊設計和構建公共語言運行庫(CLR)時,他們決定在CLR自己中實現一個線程池
    • 這樣,任何託管應用程序均可以利用線程池,即便該應用程序運行在Windows 2000以前的Windows版本(例如Windows 98)上。

1.3. CLR線程池工做過程

線程池執行任務流程併發

  • CLR初始化時,其線程池不包含任何線程。
  • 當應用程序要建立線程來執行任務時,應用程序應請求該任務由線程池線程執行。
  • 線程池知道後,將建立一個初始線程。這個新線程將與其餘任何線程進行相同的初始化。
  • 當任務完成時,線程不會銷燬本身。而是,線程將進入掛起(suspended)狀態下返回線程池。
  • 線程池已有線程能知足運算需求
    • 若是應用程序再次請求線程池,則被掛起的線程將被喚醒執行任務,不會建立新線程。
    • 只要應用程序將任務列入到線程池的速度不超過一個線程處理每一個列入的任務的速度,就節省了線程建立銷燬產生的開銷
  • 線程池已有線程不能知足運算需求
    • 若是應用程序將任務列入到線程池的速度超過一個線程處理每一個列入的任務的速度,則線程池將建立其餘線程。
    • 固然,建立新線程確實會產生開銷,可是應用程序極可能僅須要幾個線程來處理應用程序生命週期內向它拋出的全部任務
  • 線程池線程超過須要須要的算力
    • 當線程池線程自身掛起時,若是一段時間(如40秒)沒有被使用時,線程將喚醒並自行銷燬
      • 從而釋放它正在使用的全部OS資源(堆棧,內核對象等)
  • 總的來講,使用線程池能夠提升應用程序的性能。

線程池提供了四種功能:異步

  • 異步調用方法
  • 按時間間隔調用方法
  • 當發信號通知單個內核對象時調用方法
  • 異步I/O請求完成時調用方法
    • 應用程序開發人員不多使用這個功能,所以在此不作說明

要爲線程池中的任務排隊,請使用 System.Threading 命名空間中定義的 ThreadPool 靜態類。異步編程

2. 線程池解決的問題

2.1. 異步調用方法

要使線程池線程異步調用方法,您的代碼必須調用 ThreadPoolQueueUserWorkItem 方法,以下所示:函數

public static Boolean QueueUserWorkItem(WaitCallback wc, Object state);
public static Boolean QueueUserWorkItem(WaitCallback wc);

這些方法將「工做項」(和可選的狀態數據)排隊到線程池中的線程,而後當即返回。性能

  • 「工做項」只是一個委託
    • System.Threading.WaitCallback 委託類型定義以下:spa

      public delegate void WaitCallback(Object state);
  • 「狀態數據」是一個 Object 類型的數據,做爲參數傳遞給「工做項」委託
    • 沒有 「狀態數據」 參數的QueueUserWorkItem版本將 null 傳遞給WaitCallback方法
  • 最終,池中的某些線程將處理工做項,從而致使您的方法被調用

CLR的線程池將在必要時自動建立線程,並在可能的狀況下重用現有線程。該線程在處理回調方法後不會當即被銷燬。它返回線程池,以便準備處理隊列中的任何其餘工做項線程

線程池調用異步方法

using System;
using System.Threading;

class App {
   static void Main() {
      Console.WriteLine("Main thread: 列入一個異步操做.");
      ThreadPool.QueueUserWorkItem(new WaitCallback(MyAsyncOperation));

      Console.WriteLine("Main thread: 執行其餘操做.");
      // ...

      Console.WriteLine("Main thread: 暫停在這,以模擬執行其餘操做。");
      Console.ReadLine();
   }

   static void MyAsyncOperation(Object state) {
      Console.WriteLine("ThreadPool thread: 執行異步操做.");
      // ...
      Thread.Sleep(5000);    
      // 等待5s,模擬執行工做項
      // 方法返回,致使線程掛起自身,以等待其餘「工做項」
   }
}

輸出爲:

Main thread: 列入一個異步操做.
Main thread: 執行其餘操做.
Main thread: 暫停在這,以模擬執行其餘操做。
ThreadPool thread: 執行異步操做.

2.2. 按時間間隔調用方法

System.Threading 命名空間定義了 Timer 類。當構造 Timer 類的實例時,您在告訴線程池您但願在未來的特定時間回調您的方法。 Timer 類提供了四個構造函數:

public Timer(TimerCallback callback, Object state,
   Int32 dueTime, Int32 period);
public Timer(TimerCallback callback, Object state,
   UInt32 dueTime, UInt32 period);
public Timer(TimerCallback callback, Object state,
   Int64 dueTime, Int64 period);
public Timer(TimerCallback callback, Object state,
   Timespan dueTime, TimeSpan period);

System.Threading.TimerCallback 委託類型定義以下:

public delegate void TimerCallback(Object state);
  • 構造傳遞的委託調用時將構造傳遞的Object對象 state 做爲參數傳遞
  • 可使用dueTime參數來告訴線程池第一次調用回調方法以前要等待多少毫秒。
    • 當即調用回調方法設置 dueTime 參數爲 0
    • 防止調用回調方法設置 dueTime 參數爲 Timeout.Infinite0
  • 參數 period 容許您指定每一個連續調用以前等待的時間(以毫秒爲單位)
    • 爲0時,線程池將僅調用一次回調方法。
  • 構造 Timer 對象後,線程池自動監視時間
    • Timer類提供了一些方法,使您能夠與線程池進行通訊,以修改什麼時候回調該方法
    • ChangeDispose 方法
      cs public Boolean Change(Int32 dueTime, Int32 period); public Boolean Change(UInt32 dueTime, UInt32 period); public Boolean Change(Int64 dueTime, Int64 period); public Boolean Change(TimeSpan dueTime, TimeSpan period); public Boolean Dispose(); public Boolean Dispose(WaitHandle notifyObject);
      • Change 方法使您能夠更改 Timer對象dueTimeperiod
      • 使用 Dispose 方法能夠徹底和有選擇的取消回調
        • 當全部正在執行的回調完成後,經過notifyObject參數,發送信號到內核對象,取消後續執行

每2000毫秒(或2秒)調用一次方法示例:

using System;
using System.Threading;

class App {
   static void Main() {
      Console.WriteLine("每2秒檢查一次修改狀態.");
      Console.WriteLine("   (按 Enter 鍵中止示例程序)");
      Timer timer = new Timer(new TimerCallback(CheckStatus), null, 0, 2000);
      Console.ReadLine();
   }

   static void CheckStatus(Object state) {
      Console.WriteLine("檢查狀態.");
      // ...
   }
}

輸出爲:

每2秒檢查一次修改狀態.
   (按 Enter 鍵中止示例程序)
檢查狀態.
檢查狀態.
檢查狀態.
... ...

3. 當單個內核對象接收到信號通知時調用方法

在進行性能研究時,Microsoft研究人員發現許多應用程序生成線程,只是爲了等待單個內核對象接收發出的信號。

對象發出信號後,線程將某種通知發佈到另外一個線程,而後返回回來以等待對象再次發出信號。

一些開發人員甚至編寫了其中多個線程各自等待一個對象的代碼。這是對系統資源的極大浪費。
所以,若是您的應用程序中當前有等待單個內核對象發出信號的線程,那麼線程池再次是您提升應用程序性能的理想資源。

3.1. 註冊WaitHandle

System.Threading.ThreadPool 類中定義的一些靜態方法(RegisterWaitForSingleObject)。要使線程池線程在發出內核對象信號時調用方法

public static RegisterWaitHandle RegisterWaitForSingleObject(
   WaitHandle waitObject, WaitOrTimerCallback callback, Object state, 
   UInt32 milliseconds, Boolean executeOnlyOnce);

public static RegisterWaitHandle RegisterWaitForSingleObject(
   WaitHandle waitObject, WaitOrTimerCallback callback, Object state, 
   Int32 milliseconds, Boolean executeOnlyOnce);

public static RegisterWaitHandle RegisterWaitForSingleObject(
   WaitHandle waitObject, WaitOrTimerCallback callback, Object state, 
   TimeSpan milliseconds, Boolean executeOnlyOnce);

public static RegisterWaitHandle RegisterWaitForSingleObject(
   WaitHandle waitObject, WaitOrTimerCallback callback, Object state,
   Int64 milliseconds, Boolean executeOnlyOnce);

當您調用RegisterWaitForSingleObject方法時

  • waitObject 參數,須要線程池等待的內核對象
    • 能夠將引用傳遞給AutoResetEvent,ManualResetEvent或Mutex對象
  • callback 參數,須要線程池線程調用的方法
    • System.Threading.WaitOrTimerCallback 委託類型定義:

      public delegate void WaitOrTimerCallback(Object state, Boolean timedOut);
  • state 參數, callback 委託對象運行時須要的 state 參數
    • 若是不須要能夠爲 null
  • milliseconds參數, 單位:毫秒,須要線程池等待多久後向內核對象發出超時信號
    • 一般在此處傳遞-1表示無限超時
  • executeOnlyOnce參數
    • true ,則線程池線程將僅執行一次回調方法,以後線程將再也不在 waitObject 參數上等待
    • false ,則每次向內核對象發出信號時,線程池線程都會執行回調方法(這對於AutoResetEvent對象最有用)
      • 表示每次完成等待操做後都重置計時器,直到 waitObject 取消註冊

調用WaitOrTimerCallback委託類型的回調方法,bool類型的 timedOut 參數值:

  • false ,說明內核對象收到信號,致使該方法被調用
  • true ,說明在指定的時間內沒有發信號通知內核對象,超時後致使該方法被調用
    • 回調方法應執行必要的操做(超時處理)

3.2. 註銷 WaitHandle

  • 在前提到的 RegisterWaitForSingleObject 方法返回一個 RegisteredWaitHandle 對象
  • 該對象標識線程池正在等待的內核對象
    • 若是因爲某種緣由您的應用程序想要告訴線程池中止監視已註冊的 WaitHandle ,則您的應用程序能夠調用 RegisteredWaitHandleUnregister 方法:
      cs public Boolean Unregister(WaitHandle waitObject);
    • waitObject 參數,指示當全部排隊的工做項均已執行時,如何通知您
      • 若是您不想收到通知,則應爲此參數傳遞 null
      • 若是將派生自 WaitHandle 的對象引用傳遞給Unregister方法,當已註冊的WaitHandle的全部待處理工做項均已執行時,線程池將向該對象;。;(waitObject)發出信號

3.3. 代碼示例

using System;
using System.Threading;

class App {
   static void Main() {
      AutoResetEvent are = new AutoResetEvent(false);
      RegisteredWaitHandle rwh = ThreadPool.RegisterWaitForSingleObject(
         are, new WaitOrTimerCallback(EventSignalled), null, 1100, false);
      for (Int32 x = 0 ; x < 5; x++) {
         Thread.Sleep(1000);
         are.Set();
      }
      
      Thread.Sleep(2400);

      rwh.Unregister(null);
      Console.WriteLine("按 Enter 鍵中止示例程序");
      Console.ReadLine();
   }

   static void EventSignalled(Object state, Boolean timedOut) {
      if (timedOut) {
         Console.WriteLine("等待 AutoResetEvent 操做超時.");
      } else {
         Console.WriteLine("AutoResetEvent 接到信號.");
      }
   }
}

輸出爲:

; 五次 AutoResetEvent 發送信號
AutoResetEvent 接到信號.
AutoResetEvent 接到信號.
AutoResetEvent 接到信號.
AutoResetEvent 接到信號.
AutoResetEvent 接到信號.
; 等待2.4s致使兩次超時
等待 AutoResetEvent 操做超時.
等待 AutoResetEvent 操做超時.
; 註銷後不在調用委託
按 Enter 鍵中止示例程序

4. 結語

關於線程池相關的 API 不須要熟練掌握,可是咱們瞭解了線程池的設計初衷和具有的功能,爲咱們更好的理解 異步編程、並行、多線程 有很大的助益。

前一章咱們瞭解了 同步上下文 是如何編排線程執行代碼的,這一章咱們瞭解了線程池,加深了異步任務交給線程池執行的理解,後面咱們開始對 異步編程、數據流塊、Rx等咱們經常使用的技術進行講解。

相關文章
相關標籤/搜索