使用線程池與專用線程

來源:互聯網
上載者:User

高效線程使用聖典

  嚴格來講,線程的系統開銷很大。系統必須為線程分配並初始化一個線程核心對象,還必須為每個線程保留1MB的地址空間(按需提交)用於線程的使用者模式堆棧,分配12KB左右的地址空間用於線程的核心模式堆棧。然後,緊接著線程建立後,Windows調用進程中每個DLL都有的一個函數來通知進程中所有的DLL作業系統建立了一個新的線程。同樣,銷毀一個線程的開銷也不小:進程中的每個DLL都要接收一個關於線程即將“死亡”的通知,而且核心對象及堆棧還需釋放。

  如果一台電腦中只有一個CPU,那麼在某一時刻只有一個線程可以運行。Windows必須追蹤記錄線程對象,而且是不停地追蹤記錄每個線程對象。Windows不得不決定CPU下次調度哪個線程來執行。這個額外的代碼不得不每隔20ms左右執行一次。Windows使CPU停止執行一個線程的代碼,而開始執行另一個線程的代碼的現象,我們稱之為環境切換(context switch)。環境切換的開銷相當大,因為作業系統必須執行以下步驟:

  1. 進入核心模式。

  2. 將CPU的寄存器儲存到當前正在執行的線程的核心對象中。x86架構的機器上CPU寄存器佔了大約700位元組的空間;x64架構的機器上CPU寄存器佔了大約1240位元組的空間;而在IA64架構的機器上CPU寄存器佔了大約2500位元組的空間。

  3. 需要一個自旋鎖(spin lock),確定下一次調度哪個線程,然後再釋放該自旋鎖。如果下一次調度的線程屬於另一個進程,那麼此處的開銷會更大,因為作業系統必切換到虛擬位址空間。

  4. 將即將啟動並執行線程的核心對象的值載入到CPU寄存器中。

  5. 退出核心模式。

  所有上述內容都是純粹的開銷,導致Windows作業系統和應用程式的執行速度比在單線程系統上的執行速度慢。

  綜合上述所有結果可得出以下結論:應儘可能地限制線程的使用。如果建立的線程越多,給作業系統帶來的開銷就越大,所有的東西也就運行得越慢。另外,每個線程都需要資源(核心對象佔用的記憶體及兩個堆棧),所以每個線程都會消耗記憶體。

  線程還有另一個用途:可擴充性。當電腦有多個CPU時,Windows能同時調度多個線程:每個CPU運行一個線程。

CLR線程池簡介

  如前所述,建立並銷毀一個線程在時間上的開銷相當大。另外,線程多還會浪費記憶體資源,而且由於作業系統不得不在可運行線程間進行調度和環境切換,從而影響作業系統和應用程式的效能。為改進這種現象,CLR中包含管理CLR線程池的代碼。我們可以將線程池看作應用程式自己使用的線程的集合。每個進程都有一個線程池,這個線程池被該進程中的所有應用程式定義域共用。

  當CLR初始化時,線程池中還沒有任何線程。從內部實現上講,線程池維護了一系列操作請求。應用程式希望執行一個非同步作業時,可以調用一些方法線上程池的隊列中加入一個條目。線程池中的代碼將從這個隊列中提取出條目,並將該條目指派到線程池中的線程。如果線程池中沒有任何線程,就建立一個新的線程。建立一個線程會有相關的效能損失。但是,當線程池中的線程完成任務時,並不會被銷毀,而是返回到線程池中,線上程池中空閑,等待響應另外的請求。因為線程不對它自身進行銷毀,所以此處不會帶來效能損失。

  如果應用程式對線程池進行了很多的請求,那麼線程池將試圖只用一個線程來響應所有的請求。但是,如果應用程式排隊的請求超出了線程池的處理能力,線程池中將建立另外的線程。最終,應用程式排隊的請求與線程池中線程的處理能力達到一個平衡點,我們可以採用較小數量的線程來處理所有的請求,因此線程池中也就不再需要建立更多的線程。

  如果應用程式停止請求線程池,線程池中可能會有許多不做事情的線程。這種情況會浪費記憶體資源。因此,當線程池中的線程空閑超過大約2分鐘後,線程將喚醒自己,並終止自己,以釋放記憶體資源。當線程終止自己時,也會存在一個效能損失。但是,該效能損失不是很嚴重,因為線程在終止自己時,線程已處於空閑狀態,這意味著我們的應用程式當前沒有執行太多的工作。

  從內部實現上講,線程池將線程池中的線程進行分類,劃分為背景工作執行緒(worker thread)和I/O線程(I/O thread)。當應用程式請求線程池執行一個受計算限制的非同步作業(包括初始化受I/O限制的非同步作業)時使用背景工作執行緒,而I/O線程用於在受I/O限制的非同步作業完成時通知代碼。具體而言,這意味著我們需要使用非同步編程模型來進行I/O請求。

限制線程池中的線程數量

  CLR的線程池允許開發人員設定背景工作執行緒和I/O線程的最大數量。CLR保證建立的線程數量不會超過這個設定值。但永遠不要對線程池中線程的數量設定一個上限,因為饑餓和死結現象可能會發生。在CLR的2.0版預設中,背景工作執行緒的預設最大數量為機器中每個CPU25個,I/O線程最大數量設為1000個。

  System.Threading.ThreadPool類提供了幾個操作線程池中線程數量的靜態方法:GetMaxThreads(查詢線程池對線程數量的最大限制)、SetMax-Threads(設定線程數量最大限制)、GetMinThreads(查詢線程池對線程數量的最小限制)、SetMinThreads(設定線程數量最小限制)、GetAvailable-Threads。

  強烈建議不要調用SetMaxThreads方法修改線程池中線程數量的限制,因為這會導致損害應用程式的執行效能。

  CLR的線程池試圖避免過快地建立額外的線程。具體而言,線程池試圖避免每隔500ms就建立一個新的線程。這對某些開發人員而言,引發了一個問題,因為隊列中的任務無法得到及時地處理。要處理此問題,可以調用SetMinThreads方法設定線程池中擁有線程的最低數量。調用該方法後,線程池將很快地建立這麼多的線程,並且當隊列的任務繼續增加,所建立的所有線程都被使用後,線程池還會按照每隔500ms的時間繼續建立額外的線程。預設情況下,線程池中背景工作執行緒和I/O線程的最小數量被設為2,這個值可以通過調用GetMinThreads方法獲得。

  最後,可以通過調用GetAvailableThreads方法來獲得線程池中可以增加的額外線程的數量。該方法的傳回值為線程池中可以擁有的線程的最大數量減去線程池中當前所擁有的線程數量。這個值僅在返回的那一刻有用,因為在方法返回後,線程池中可能已經增加了許多線程,或有些線程可能已被銷毀。

使用線程池執行受計算限制的非同步作業

  受計算限制的操作是需要進行計算的操作。如,試算表應用程式中可計算的單元。理想情況下,受計算限制的操作不會執行任何非同步I/O操作,因為所有的非同步I/O操作在底層硬體執行工作時都將掛起調用線程。應該盡量使線程運行,因為掛起的線程不再繼續運行但仍然使用系統的資源。

  為了將一個受計算限制的非同步作業加入到線程池的隊列中,一般可以使用ThreadPool類中定義的下述方法:

static bool QueueUserWorkItem(WaitCallback callback);
static bool QueueUserWorkItem(WaitCallback callback, object state);
static bool UnsafeQueueUserWorkItem(WaitCallback callback, object state);

  上述方法將一個“工作項目”(及可選的狀態資料)加入到線程池的隊列中,然後這些方法就會立即返回。工作項目僅僅是一個由CallBack參數標識的方法,線程池中的線程將調用該方法。該方法可以只傳遞一個單獨的由state(狀態資料)參數指定的參數。沒有state參數的QueueUserWorkItem方法為回呼函數傳遞null。最終,線程池中的一些線程將執行工作項目,從而導致我們的方法被調用。我們寫的回調方法必須匹配System.Threading.WaitCallback委託類型,它的定義方式如下所示:

delegate void WaitCallback(object state);

  下面的代碼示範了線程池中的線程如何非同步呼叫一個方法:

using System;
using System.Threading;

public static class Program
{
public static void Main()
{
Console.WriteLine("Main thread: queuing an asynchronous operation");
ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5);
Console.WriteLine("Main thread: Doing other work here ...");
Thread.Sleep(10000); //類比其他工作10秒鐘
Console.WriteLine("Hit <Enter> to end this program ...");
Console.ReadLine();
}

//該方法的簽名必須與WaitCallback委託類型匹配
private static void ComputeBoundOp(object state)
{
//該方法由線程池中的線程執行
Console.WriteLine("In computeBoundOp: state={0}", state);
Thread.Sleep(1000); //類比其他工作1秒鐘

//在該方法返回後,線程就回到線程池中,然後等待執行另一個任務
}
}

  如果回調方法拋出的異常是未處理異常,那麼CLR將終止進程。

  ThreadPool類有一個UnsafeQueueUserWorkItem方法。該方法與平時調用的QueueUserWorkItem方法非常相似。下面先簡單介紹一下這兩個方法的區別:試圖訪問一個受限資源(如開啟一個檔案)時,CLR將執行一個代碼訪問安全(Code Access Security,CAS)檢查。也就是說,CLR將檢查調用線程的呼叫堆疊中的所有程式集是否都有訪問資源的許可許可權。如果有一些程式集沒有所需的許可許可權,CLR將拋出一個SecurityException異常。假設正在執行代碼的線程所在的程式集沒有開啟檔案的許可許可權,那麼線上程試圖開啟檔案時,CLR將拋出一個SecurityException異常。

  為讓線程繼續運行,線程可以線上程池的隊列加入一個工作項目,讓線程池中的線程來執行開啟檔案的代碼。當然這必須在擁有合適許可許可權的程式集中進行。這種“工作區”智取安全許可權的現象可以允許懷惡意的代碼對受限資源進行嚴重破壞。為阻止這種獲得安全許可權的方式,QueueUserWorkItem方法內部遍曆調用線程的堆棧,並捕獲所有被授予的安全許可權。然後,當線程池中的線程開始執行時,這些許可權再與線程結合。因此,線程池中的線程以調用QueueUserWorkItem方法的線程相同的許可權集來完成運行。

  遍曆線程的堆棧並捕獲所有的安全許可權與效能緊密相關。如果希望改進受計算限制的非同步作業的排隊效能,可以調用UnsafeQueueUserWOrkItem方法。該方法只將工作項目加入到線程池的隊列中,而不遍曆調用線程的堆棧。最後結果是這個方法比QueueUserWorkItem方法執行得快,但它在應用程式中開啟了一個潛在的安全性漏洞。僅當可以確認線程池中的線程執行的代碼不觸及受限資源時,或確信接觸這部分資源不會出現問題時,我們才可以調用UnsafeQueueUserWork-Item方法。同樣,還需注意調用該方法需要使SecurityPermission的ControlPolicy標記和ControlEvidence標記開啟,可阻止未信任的代碼偶然或故意提升它的許可許可權。

使用專用線程執行受計算限制的非同步作業

  強烈建議大家盡量多用線程池來執行受計算限制的非同步作業。但在有些情況下,我們可能希望顯式建立一個線程,專門用於執行特定的受計算限制的非同步作業。一般情況下,如果即將執行的代碼需要線程處於一個特定的狀態(與線程池中線程的普通狀態不同),那麼就希望建立一個專用的線程。如:希望線程以一個特殊的優先順序運行(所有線程池中的線程都以普通優先順序運行,而且我們不應該修改線程池中線程的優先順序),就需要建立一個專用的線程。再如:希望讓一個線程成為前台線程(所有線程中的線程都是後台線程),也可以考慮建立並使用自己的線程,從而阻止應用程式的“死亡”,直到線程完成任務。如果受計算限制的任務啟動並執行時間特別長,也應該使用專用線程,這樣,我們就不必讓線程池的邏輯去費力判斷是否還需建立額外的線程。最後,如果我們希望啟動一個線程,然後通過調用Thread的Abort方法中斷該線程的話,應該使用一個專用線程。

  為建立一個專用線程,我們可構建一個System.Threading.Thread類的執行個體(以方法的名稱作為構造器的參數)。下面是構造器的原型:

public sealed class Thread : CriticalFinalizerObject, ...
{
public Thread(ParameterizedThreadStart start);
}

  參數start用來標識專用線程的方法即將執行,這個方法必須與委託ParameterizedThreadStart的簽名相匹配:

delegate void ParameterizedThreadStart(Object obj);

  可看出,ParameterizedThreadStart委託的簽名與WaitCallback委託的簽名相同。這意味著使用一個線程池中的線程或使用一個專用線程就可以調用相同的方法。

  構建一個Thread對象並不建立一個作業系統線程。為實際建立一個作業系統線程,並讓它開始執行回調方法,我們必須調用Thread的Start方法。如下所示:

using System;
using System.Threading;

public static class Program
{
public static void Main()
{
Console.WriteLine("Main thread: starting a dedicated thread " + " to do an asynchronous operation");
Thread dedicatedThread = new Thread(ComputeBoundOp);
dedicatedThread.Start(5);

Console.WriteLine("Main thread: Doing other work here...");
Thread.Sleep(10000); //類比其他工作10秒

dedicatedThread.Join(); //等待線程終止
Console.WriteLine("Hit <Enter> to end this program...");
Console.ReadLine();
}

//該方法的簽名必須與ParameterizedThreadStart委託匹配
private static void ComputeBoundOp(object state)
{
//該方法由一個專用線程執行
Console.WriteLine("In ComputeBoundOp: state = {0}", state);
Thread.Sleep(1000); //類比其他工作1秒
}
}

  注意,Main方法調用了Join方法,而Join方法導致調用線程停止執行任何代碼,直到由dedicatedThread標識的線程自己銷毀自己或被終止。使用ThreadPool的QueueUserWorkItem方法將非同步作業排隊時,CLR沒有提供內建的方法來判斷操作是否完成。而Join方法卻在我們使用專用線程時為我們提供了這種能力。但是,如果需要知道操作是在什麼時候完成的,就不應該使用專用線程來取代QueueUserWorkItem方法,而應該使用APM。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.