程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> CLR全面透徹解析:CLR中的線程管理

CLR全面透徹解析:CLR中的線程管理

編輯:關於.NET

本專欄基於 CLR 線程系統和任務並行庫的預發布版本撰寫而成。所有信息均有可能發生變更。

當前進行的從單核體系結構到多核體系結構的技術變革帶來了諸多好處。舉例來說,在線程環境中, 如果有效使用多個線程,便可通過使用多個核和並行性提高性能,例如,使用多線程對數據庫進行多個獨 立查詢的 ASP.NET 應用程序。

但是,使用多個核會帶來一些新的問題。您可能會看到編程和同步模型變得更加復雜,您需要控制並 發,而要調整和優化性能將會更加困難。此外,對於影響並發應用程序性能和行為的許多新因素,我們還 不是很了解。因此,優化性能變得富有挑戰性,尤其是在優化目標不是某一特定應用程序,而是面向所有 應用程序時。

CLR 中的線程是並發環境的一個示例,其中許多因素(如多核體系結構引入的因素)都可能影響並發 的行為和性能。鎖爭用、緩存爭用以及過度上下文切換只是這些因素中的一部分。目前在 CLR ThreadPool 方面進行的工作旨在通過將並行框架集成到運行時並在內部處理一些最常見的問題,使開發 人員可以更輕松地利用並發性和並行性來獲得更高的並發級別和更好的性能。

在本專欄中,我們將介紹在優化多線程托管代碼(尤其是位於多處理器硬件中的托管代碼)過程中開 發人員遇到的一些常見問題和需要注意的重要因素,我們還將介紹 ThreadPool 的未來更改將如何解決上 述問題,以及幫助減輕編程人員的工作負擔;此外,我們還將討論並發控制的一些重要方面,以幫助您了 解應用程序在高度多線程化的環境中的行為。我們在介紹以下內容時假設您熟悉 CLR 中的並發、同步和 線程的基本概念。

並發的常見問題

大多數軟件在設計上都適合單線程執行。其部分原因是,單線程執行的編程模型可降低復雜性,並且 比較容易編碼。現有的單線程應用程序對多核化境的適應能力極差,不能使用多個核。更糟糕的是,要使 單線程模型適應多核環境也不很一件簡單的事情,其中許多原因是可預測的,如鎖爭用和資源爭用、爭用 條件和資源缺乏等同步問題。其他原因目前還不是很清楚。例如,如何確定特定類型(和大小)的工作負 荷在任意給定時間的最佳並發級別(線程數)?

想一下設計為在單核計算機上運行的 CLR 中的線程系統:任務(工作項)逐個排隊,形成一個列表, 該列表受一個鎖保護。然後,每個線程按順序占用隊列中的項目。當將該線程系統遷移到多核體系結構時 ,如果鎖上存在大量對更高並發級別的爭用時,其性能就會降低。有趣的是,Kenny Kerr 在 2008 年 10 月在“借助 C++ 進行 Windows 開發”專欄中曾發表過“探索高性能算法”一文,其中他說道“單處理器 上設計良好的算法通常可以勝過多處理器上的低效實現”,這句話表明了僅添加更多的處理器並不總能改 善性能。

在本文的後部分內容中,我們將現有的線程系統統稱為 ThreadPool,將工作項的概念統稱為任務隊列 中的項(如 QueueUserWorkItem)。在單核計算機中,通過在線程之間分配處理器的時間,可按照詳細的 任務計劃管理並發操作。既然存在多個核,您還必須考慮如何在核之間公平地分配工作、考慮基礎內存層 次結構以確保正確性和性能,以及決定如何控制和利用更高的並發級別。

公平性和性能

任務公平性和性能之間的矛盾在並發環境中是巨大的:幾乎不可能既獲得合理的任務計劃,又獲得整 體較高的性能。例如,隊列中存在大量工作項等待運行。為公平起見,應該按照每個工作項的到達順序來 運行(使用)工作項。但是,這樣並不能提供最佳的性能。

從操作系統這一方面考慮,獲得出色性能的一種行之有效的方法是將“熱”項目(新術語)保留在緩 存中,從而不必大費周折地將其傳輸到主內存,但這種方法與前面按順序使用項目的想法相沖突。在緩存 中保留最近信息的最好方法就是首先取消隊列中最後幾個項目的排隊順序。

ThreadPool 環境主要側重於公平性,但即將發布的版本的目標是獲得更好的性能。為此,即將發布的 版本中將包含工作竊取算法等策略。在工作竊取方法中,存在多個隊列(如一個處理器對應一個隊列), 並且隊列中的“冷”項目可以被“竊取”和運行(例如,在其他處理器中)。此方法可以在保留一些公平 性的同時改善性能,這對許多並發應用程序都是有利的。

鎖爭用

出於各種原因,並發環境會頻繁地遇到鎖爭用情況。例如,在 ThreadPool 中,每次將工作項添加到 任務隊列和每次嘗試取消排隊時,都會獲得鎖。當存在許多小型工作項,且不得不頻繁地使用鎖時,便會 發生爭用:當工作到達且短時間內大量排隊時(因為在大量排隊期間通常會頻繁使用鎖),以及當存在錯 誤的並發控制時。只要有工作變為可用狀態,ThreadPool 就會喚醒所有線程,因此如果存在許多線程, 但隊列中沒有工作,此時有一個工作項到達,則所有線程便會同時蘇醒並嘗試獲得鎖。

由於線程在等待鎖時會旋轉,這會導致您認為線程繁忙,此問題會變得更復雜;在此期間,如果又排 隊了一些工作項,則會添加更多的新線程,並且它們還會嘗試獲得鎖,這就形成了一個循環。一些 ASP .NET 應用程序經常出現此問題。

上一情況的解決方法是,進一步限制我們每次釋放的線程數。遺憾的是,這有時會導致釋放的線程過 少,從而使整個 ThreadPool 發生停滯。當前為緩和爭用在 ThreadPool 上進行的工作包括刪除某些鎖。

同步

您必須優化自己的同步設計,但使用同步基元並不是一件容易的事。您必須選擇同步基元,並決定是 使用讀取鎖定還是寫入鎖定,具體取決於這些操作中每個操作的頻率。此外,與在單處理器計算機上使用 基元相比,在多核系統上使用基元的成本有時要高得多。

有關此情況的一個示例是單個處理器上的互斥鎖,該互斥鎖合理執行,並可以提高工作負荷分配的公 平性,但是,當將應用程序移動到多核環境時,公平性會降低。在這種情況下,互斥鎖強制執行的公平性 開始損害性能,這是因為工作無法執行時,它必須等待;因此,在某些情況下,例如,在 Microsoft .NET Framework 中使用監視器時,您最好停止旋轉,而不是等待獲得鎖。(有關性能和線程同步的詳細 信息,請參閱“並發事件”專欄的“注重性能的線程同步”。

在多核環境中計劃任務這一主題涉及的范圍極其廣泛,本專欄不會對其進行全面介紹。但是,有些問 題對於所有實現來說都是比較常見的。例如,如何選擇您的工作使用策略?答案取決於應用程序。您可以 確定排在隊列最後的工作項包含最近的活動,因此,在使用排在隊列前面的工作項之前先使用這些工作項 可能獲得更好的性能。

工作負荷的類型和大小

性能的好壞取決於應用程序工作負荷的類型或大小。您可能已經注意到,受 CPU 限制的工作負荷存在 較多的計劃和同步問題,而不受 CPU 限制的工作負荷更有可能影響並發級別控制。此外,我們已在前面 提到,擴展應用程序使其可以使用多個內核可能比較困難。

以下示例說明了此問題。一個簡單實驗應用程序使用 ThreadPool 在不同數量的內核上運行(在同一 計算機上使用不同的進程相似性掩碼)。圖 1 顯示了一些具有代表性的結果。工作負荷發生了變化,我 們可以借此探究使用更多內核是否能夠很好地解決此問題。

圖 1 增加內核時不同工作負荷的吞吐量結果

“實驗性”工作負荷 0 比較大(且更實際)的工作負荷效果要好;從這種意義上講,使更多內核承擔 更大工作負荷並不能正確地擴展應用程序,使其運行效果更好。但是,最下方與較大工作負荷對應的線( 淺藍和深藍)表明,吞吐量隨內核的增加而增加,而實驗性工作負荷零的吞吐量隨內核的增加而略微減少 。此外,較大的工作負荷可能會遇到更多的鎖爭用。可以想象,使用單一的通用優化技術解決所有這些缺 陷比較困難。

內存層次結構問題

鑒於內存層次結構是性能優化過程中最重要的問題之一,因此充分利用內存位置(尤其在使用大量內 核的情況下)是不可或缺的。在非一致性內存訪問 (NUMA) 體系結構中,訪問“遠距離”節點中的內存位 置所需的時間較長並且可能嚴重影響性能。了解基礎內存層次結構對於計劃來說非常重要。例如,使內存 訪問更適用於多處理器環境的方法是,在同一處理器中維護執行特定任務的線程。ThreadPool 不支持 NUMA,但添加的策略(如內部工作竊取)可以利用內存位置。由於本地排隊和取消排隊,相似的任務往往 保留在同一節點中。

請記住,通常情況下高效利用緩存可以從很大程度上改進性能。(有關內存層次結構的詳細信息,請 參閱“模式轉變:並行編程方面的設計注意事項”和“CLR 全面透徹解析:利用並發操作實現可伸縮性” 。

危險優化

更有挑戰性的是,有時候引入某種旨在改進性能的邏輯可能會獲得相反效果。典型的例子是此類改進 旨在通用優化性能的情況;對於某些應用程序而言,它只會增加大量開銷,從而導致性能下降。

例如 ,如果為某個應用程序添加了各種同步機制使其在多核中起作用(例如,合理地分配工作),但執行同步 會增加復雜性,從而導致簡單的應用程序產生不必要的開銷,在這種情況下使用更為簡單的解決方案效果 會更好。在高度並發的應用程序中,識別重要的優化參數相當困難。例如,最大限度地利用 CPU 不一定 必然導致吞吐量增加;雖然在單處理器計算機中確實如此,但在多核計算機中,有時使線程“浪費”一些 時間比最大限度地利用 CPU 的效果更好。

噪音

噪音已經成為影響性能優化的另一個因素。本文的所有實驗都在專用的計算機上進行(同時沒有運行 會產生重大開銷的應用程序)。但是,盡管沒有太多的噪音,還是會有難以理解的意外變化出現。

在 實際的系統中,通常有多個應用程序(因此有更多線程)爭用資源。對於 ThreadPool 來說,甚至同時執 行的不同應用程序域都有可能產生噪音。

並發控制

環境中的線程數(並發級別)非常重要,因為它會影響此環境及其應用程序的行為和性能。例如,對 於密集占用 CPU 資源的應用程序來說,一個內核對應一個線程的低並發級別是最理想的選擇(此外,系 統中設置的最小線程數通常與內核數相同)。另一方面,高並發級別可能會造成過多的上下文切換或資源 爭用。

圖 2 顯示了並發級別如何影響應用程序性能的典型示例。(請注意,藍點是在各個並發級別處捕獲的 觀察結果。黑色線條顯示適合這些實驗的曲線,以幫助我們形象地理解描述此行為的函數類型。)我們使 用吞吐量在此上下文中衡量性能,吞吐量以每秒完成的工作項數衡量。

圖 2 吞吐量與並發級別的函數關系

此圖使用在特定時間內運行工作項的實驗性應用程序生成(每個工作項的執行時間為 100 毫秒,其中 10 毫秒的 CPU 使用時間和 90 毫秒的等待時間)。此實驗在雙核 2GHz 的計算機上多次了運行,以收集 數據和評估可能存在的噪音和變化。理論上,當並發級別為 20 時,此計算機的最大吞吐量為每秒鐘完成 200 個工作項。但是,我們可以從實驗中看到在並發級別快要達到 20 之前,吞吐量達到峰值 150,隨後 呈下降趨式。吞吐量值下降的主要原因在於上下文切換。

在此實驗中,CPU 的使用率已達到最大限度(但是,在特定時刻後才出現最佳吞吐量)。如果 CPU 使 用率達到 100%,則表示正在為實際未使用的線程從內存中提取數據。表面看來,應用程序正在工作,而 實際上是在浪費 CPU 時間。這種情況也會發生在單處理器環境中,但是在多核環境中更為明顯。

我們可以看出,不完善的並發控制可能會使某些問題頻繁出現,如果應用了並行性,這種情況會更加 突出。另外,在多核環境中,並發管理多個任務將更具挑戰性。

有一個示例與內存層次結構問題有關。根據圖 2 中的示例得出了一個簡單基本原理,即將並發級別增 加到某一點可能會產生好的效果;但是,在多核環境中,確定何處需要使用線程並非易事。例如,如果一 個處理器由於某種原因(例如,不合理的計劃)而過載,而其他處理器並未充分利用,則總吞吐量可能會 很低,而新添加的線程可能會被錯誤地分配到錯誤的處理單元。

相反,如果並發級別非常高,但總吞吐量開始下降,則可以決定開始刪除線程,但是刪除這些線程的 處理器可能實際上正是需要他們的處理器,或者是需要添加更多線程來提高吞吐量的處理器。

圖 3 顯示輕易地更改並發級別如何導致意外行為。此實驗包括一系列使用不同的工作量執行的測試應 用程序的運行過程(工作項),以及每個工作項的執行時間(工作負荷)。

圖 3 更改並發級別可能導致意外行為

圖 3 的上半部分是在 ThreadPool 上運行的此次測試的結果。吞吐量很低,但顯示了所有試用版之間 的一些一致性。圖 3 的下半部分顯示了通過對並發控制邏輯進行實質性調整以嘗試改進添加和刪除線程 的方式,所獲得的結果。我們可以看到吞吐量中存在許多變體,並且無論工作量是多少,調整似乎都將導 致更多的噪音,而不是更好的控制。

某些點看起來似乎有所改進,以期達到特定的值組合,這表明需要根據應用程序的類型進行不同類型 的並發控制調整。

解決此問題的一種可行方法是,合並某種動態優化邏輯以基於應用程序調整並發級別(例如,使用統 計方法)。對並發控制和優化方法的進一步檢查也超出了我們當前的討論范圍。

適當的優化可能會改善性能。下面提供了一個顯示類似實驗的鼓舞性示例,其中專門針對此測試應用 程序手動調整了 ThreadPool 中的許多因素和參數。

性能結果如圖 4 所示。改進(由紅色線條表示)主要是添加更多個線程(可快速提高並發級別)的結 果。在 ThreadPool(由藍色線條表示)中,當認為有必要提高並發級別時,僅會添加一個線程(在此實 驗中,每隔 250 分鐘進行一次檢查)。而且,在某一時間後,將不再添加任何線程,因為不會再有任何 改進。

圖 4 手動優化 ThreadPool 獲得的改進在此實驗中,會對每個工作項進行排隊,使其都執行 CPU 工 作,然後在一段時間內對其進行阻止。每 30 秒會更改一次阻止時間:在前 30 秒內,阻止將持續 1 分 鐘,然後是 2 分鐘,然後再切換回 1 分鐘。在 90 秒標記處,工作將停止 10 秒。之後,再重復上一個 阻止循環。藍色線條表示使用現有 ThreadPool 的性能結果。紅色線條表示通過手動優化此 ThreadPool 獲得的改進。

關於獲得並行性的最後一個注意事項

引入並行性可提高性能,但是將並行概念和算法合並到現有應用程序並非是一件簡單的事情。(例如 ,這種方法有許多缺陷,如“並發危險:解決多線程代碼中的 11 個常見的問題”中所述。)

任務並行庫 (TPL) 是 .NET Framework 的擴展,由並行計算平台團隊開發,可提供一種公開並發性的 方法並支持利用並行性。MSDN 上目前已提供了 TPL 和有關此開發的詳細信息。

當然,利用公開的並行性對普通的編程人員來說仍然不是完美的解決方法,因為仍有許多需要考慮的 因素。從圖 1 中可以看到,應用程序並不總是能夠正確地進行調整。當使用“分割-解決”技術(遞歸性 )並行化一個問題時,也是如此。這可能比較棘手,因為這些算法是沒有啟用可重輸入代碼的 ThreadPool 中的死鎖的常見根源。最後,考慮一下用於以某個特定順序計算結果的串行應用程序的常見 情況。當並行化此應用程序時,可能會以不同於並行任務順序的順序計算結果。然後,由編程人員根據需 要對結果進行整理,以確保正確性和功能效果。

調整程序以使其在並發和並行環境中執行並非是一件容易的事情。通用優化甚至更困難,並且它無法 保證所有應用程序都獲得最佳行為。了解並發環境的基礎結構有助於您確定要優化的參數和要采用的最佳 編程策略。現在為 ThreadPool 的將來版本進行的工作包括嘗試減少執行手動優化的需要。

請將您想詢問的問題和提出的意見發送至 [email protected]

Erika Fuentes 博士是 CLR 團隊的項目經理,負責並發性核心操作系統方面的工作。她曾發表有關統 計學、適應性系統和科學計算領域的學術文章。

Eric Eilebrecht 是 CLR 團隊的高級軟件開發工程師,負責核心操作系統方面的工作。他在 Microsoft 就職已有 11 年,在加入 CLR 團隊之前一直負責 Hotmail 的後端存儲系統工作。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved