多執行緒程式設計的8個規則

2022-08-09 17:15:11 字數 3932 閱讀 7130

在intel,並行化技術主要有四個步驟:分析,設計與實現,除錯以及效能調優。這些步驟用來對一段序列**進行並行化。儘管這四個步驟中的第

一、三、四步都已經有了很多相關文件,但是關於怎樣進行設計與實現的卻不多。

並行程式設計更像是一門藝術,而不是一門科學。這裡將會給出八條設計多執行緒程式的簡單規則,你可以把他們一一放進你的多執行緒程式設計百寶箱中。通過參考這些規則,你能寫出高質量、高效率的多執行緒程式。我努力試著將這些規則按照(半)時間順序組織起來,但是它們之間並沒有硬性的先後順序。就像「別在泳池邊奔跑」和「別在淺水區跳水」一樣,兩個都是好主意,但是後者也能放在前者的前面,反之亦然。

規則一:找到真正不相關的計算任務

如果你將要執行的運算任務相互之間不獨立的話,你是不可能將它們並行化的。我可以很容易的舉出一些真實世界中相互獨立的任務如何為了達成同乙個目的而工作的例子。比如說乙個***出租店,它先把收到的求租電影的訂單分給員工們,員工再從存放電影***的地方根據訂單找到影片拷貝。當乙個員工取出一張古典**喜劇的拷貝時,他並不影響另乙個尋找最近科幻電影大作的員工,也不影響另乙個尋找某熱門犯罪連續劇第二季花絮的員工(我假設所有不能被滿足的訂單在遞交給***出租店之前就已經被處理過了)。同樣的,每個訂單的打包和郵遞工作也不會影響其他訂單的查詢、運送和處理工作。

你也可能會遇到某些不能被並行化的而只能序列執行的計算任務,它們大多數是因為迴圈之間或者計算步驟之間有依賴關係從而導致它們只能按照特定的順序序列執行。乙個很好的例子是馴鹿懷孕的過程。通常馴鹿需要八個月來生小馴鹿,你不可能為了早點生個小馴鹿就讓八個馴鹿來一起來生,想乙個月就生出乙個來。但是,如果聖誕老人希望盡快的擴充雪橇隊伍,他可以讓八隻馴鹿一起生,這樣八個月後就能有八隻小馴鹿了(注:可以理解為儘管單個任務的執行時間沒縮短,但是吞吐量卻大了)。

規則二:盡可能地在最高層進行並行化

如果你的程式熱點的計算任務能通過庫函式呼叫來完成,強烈建議你考慮使用同等功能的庫函式,而不是呼叫自己手寫的**。即使是序列程式,「重新造輪子」來完成已經被高度優化的庫函式實現了的功能仍不是乙個好主意。許多的庫,例如intel math kernel library(intel mkl)和intel integrated performance primitives (intel ipp),提供了能更好的利用多核處理器的並行版本的函式。

比使用並行版本的函式庫更重要的一點是:我們需要確保所有的庫函式呼叫都是執行緒安全的(thread-safe)。如果你已經把你序列**中的程式熱點替換成了乙個庫函式呼叫,你仍有可能在呼叫樹(call tree)的更高層上發現能把程式分解成獨立的計算任務的**段。當你有好幾個並行的計算任務,並且它們都同時呼叫了庫函式(特別是第三方函式庫),那麼函式庫中引用並更新共享變數的函式可能會造成資料競爭(data race)。記得好好檢查你在並行程式設計中所呼叫的函式庫的文件中關於執行緒安全性的描述。當你在設計和編寫自己的用於並行執行的函式庫時,請務必確保函式是可重入(reentrant)的。如果不能確保的話,你應該給共享的資源加上同步機制。

規則五:使用合適的多執行緒模型

如果並行版的函式庫不足以完成程式的並行化,而你又想使用可以自己控制的執行緒,在隱式的多執行緒模型能滿足你的功能需求的前提下請盡量使用該模型(例如openmp或者intel thread building block)而不是顯式的多執行緒模型(例如pthread)。顯式的多執行緒模型確實能提供對執行緒的更精確的控制。但是,如果你僅僅是想把你的計算密集型迴圈給並行化,或者你不需要顯式多執行緒模型提供的諸多特性,那麼我們最好還是能滿足需要就好。實現的複雜度越高,犯錯誤的機率就越大,以後**的維護難度也會越大。

openmp採用的是資料分解的方法,它尤其適合並行化那些需要處理大量資料的迴圈。儘管這種型別的並行化可能是唯一一種你能引入的並行模式,但是可能還會有其他的要求(例如由你的雇主或者管理層所決定的工程方案)讓你不能使用openmp。如果是那樣的話,我建議你先使用openmp來快速開發出並行化後的模型,估算一下可能的效能提公升、可擴充套件性以及大概需要多少時間才能把這些序列**用顯式多執行緒庫給並行化。

規則六:永遠不要假設具體的執行順序

在序列程式中我們可以非常容易地**某個程式的當前狀態結束之後它會變成什麼狀態。然而,多個執行緒的執行順序卻是不確定的,它是由作業系統的排程器(scheduler)決定的。這意味著我們不可能準確的**兩個執行狀態之間多個執行緒的執行順序,甚至連**哪個執行緒會在下一步被排程執行也不能。這樣的機制主要是為了隱藏程式執行時的延遲,特別是當執行的執行緒的數量多於核心的數量時。例如,如果乙個執行緒因為要訪問不在cache中的位址,或者需要處理乙個i/o請求而被阻塞了(blocked),那麼作業系統的排程器就會把該執行緒排程到等待佇列裡,同時把另乙個等待執行的執行緒排程進來並執行它。

資料競爭(data race)就是由這種排程的不確定性造成的。如果你假設乙個執行緒對共享變數的寫操作會在另乙個執行緒對該共享變數的讀操作之前完成,你的**可能會一直正確,有可能有些時候會正確,也有可能從來都不會正確。如果你足夠幸運的話,有時候在乙個特定平台上每次你執行這個程式時執行緒的執行順序都不會改變。但是系統間的每個不同(例如資料在磁碟上儲存的位置,記憶體的速度或者插座中的交流電源)都有可能影響執行緒的排程。對一段需要特定的執行緒執行順序的**來說,如果僅僅依靠樂觀的估計而不採取任何實質性的措施的話,很有可能會受到資料競爭,死鎖等問題的困擾。

從效能的角度來講,最好的情形當然是讓所有的執行緒盡可能沒有約束的執行,就像比賽中的賽馬或獵犬的一樣。除非必要的話,盡可能不要規定乙個特定的執行順序。你需要找到那些確實需要規定執行順序的地方,並且實現一些必要的同步方法來調整執行緒間的執行順序。

拿接力賽跑來說,第一棒的選手會竭盡全力的奔跑。但是為了成功的完成接力賽,第二個,第三個和最後一棒都需要先等到拿到接力棒之後才能開始跑他們的賽段。接力棒的交接就是他們的同步機制,這樣就確保了接力過程中的「執行」順序。

規則七:盡可能使用執行緒本地儲存或者對特定的資料加鎖

當比較序列或者並行程式的效能的時候,執行時間就是衡量的首要標準。程式設計師會根據演算法的時間複雜度來進行選擇。時間複雜度和乙個程式的效能是息息相關的。它的含義就是,當其他的一切條件都一樣時,完成同樣功能的時間複雜度為o(nlogn)的演算法(例如快速排序)要比o(n^2)的演算法(例如選擇排序)要快。

在並行程式中,擁有更好的時間複雜度的演算法也會更快一些。然而,有些時候時間複雜度更好的演算法卻不是很容易被並行化。如果演算法的熱點不太容易被並行化的話(而且在呼叫棧的更高層中你又找不到能很容易被並行化的熱點),那麼你可以嘗試換乙個稍微慢一點但是卻更容易被並行化的演算法。當然,還有可能一些其他的改動措施也能讓你比較輕鬆的把某一段**給並行化了。

這裡我們可以給出乙個線性代數中兩個矩陣相乘的例子。strassen的演算法擁有最好的時間複雜度:o(n^2.81)。這當然比傳統的三重迴圈的o(n^3)的演算法要好。strassen的演算法把每個分成四部分,然後進行七次遞迴呼叫來對n/2 x n/2的子矩陣進行乘運算。如果想把這七次遞迴呼叫並行化的話,我們可以在每次的遞迴呼叫的時候建立乙個新執行緒來進行運算,直到子矩陣到達乙個預設的大小為止。這樣的話執行緒的數量就會指數級的成倍增長。隨著子矩陣越來越小,給新建立的執行緒分配的計算任務就會越來越少。還有另一種方法,就是先建立乙個有七個執行緒的執行緒池。七次子矩陣相乘的運算任務可以分別分配給這七個執行緒以完成並行化。這樣的話執行緒池就會跟序列版本的程式一樣遞迴呼叫strassen演算法來對子矩陣進行乘運算。然而,這種方法的缺點就在於對乙個擁有大於八個核的系統來說,永遠只有七個核在工作,其他的資源都被浪費了。

另乙個更容易被並行化的矩陣乘法就是三重迴圈的演算法了。我們可以有很多方法來對矩陣進行資料分解(按行分解,按列分解或者按塊分解)然後再把它們分配給不同的執行緒。通過用openmp在某一層迴圈中加上編譯指示,或者用顯式執行緒模型實現矩陣分割,我們很容易的就能完成並行化。只需要更少的**改動就可以對這個簡單的序列演算法完成並行化,並且**的整體結構改動也會比strassen演算法要少很多。

總結

我們已經列出了八條簡單的規則,在把序列程式並行化的過程中你應該時刻記住它們。通過遵循這些規則以及一些實際的程式設計規則,你應該可以更容易的創造更健壯的並行化解決方案,同時能包含更少的並行化時的問題,以及在更短的時間裡得到最好的效能。

多執行緒程式設計的8個規則

另外一篇 在intel,並行化技術主要有四個步驟 分析,設計與實現,除錯以及效能調優。這些步驟用來對一段序列 進行並行化。儘管這四個步驟中的第 一 三 四步都已經有了很多相關文件,但是關於怎樣進行設計與實現的卻不多。並行程式設計更像是一門藝術,而不是一門科學。這裡將會給出八條設計多執行緒程式的簡單規...

多執行緒程式設計 8 Timer

net有很多的計時器 這個timer屬於作業系統內部實現,最輕量級,以委託方式實現.這種物件要記得呼叫dispose方法釋放 private static void threadingtimer static void timeaction object o datetime.now 以.net元件...

多執行緒程式設計5個建議。

多執行緒。以下複製於ibm技術如文件。詳情可以去linux多執行緒開發經驗。多執行緒開發在 linux 平台上已經有成熟的 pthread 庫支援。其涉及的多執行緒開發的最基本概念主要包含三點 執行緒,互斥鎖,條件。其中,執行緒操作又分執行緒的建立,退出,等待 3 種。互斥鎖則包括 4 種操作,分別...