ThreadPool(線程池)介紹


>>返回《C# 並發編程》

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 委托類型定義如下:
      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 方法
      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 方法:
      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等我們常用的技術進行講解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM