深入執行緒同步

Spark++發表於2023-04-03

剛接觸執行緒的時候懵懵懂懂,懵懵逼逼,只是依稀記得執行緒需要同步,至於這麼做的原因好像是避免執行緒由於對資料的競爭導致不可預知的結果。隨著頭髮日漸稀疏,對執行緒同步的理解也不斷加深了。

什麼是執行緒同步


執行緒同步是指多個執行緒之間的協調同步,按照一定的次序進行執行。Linux中的執行緒同步機制主要有互斥鎖、自旋鎖、讀寫鎖和條件變數四種。互斥鎖與自旋鎖在使用形式上比較類似,都是前一個執行緒在加鎖後會阻止後來想要加鎖執行緒被阻塞或者返回錯誤。我們可以把讀寫鎖比作前兩者的延伸,這個機制允許不同執行緒在同一讀寫鎖上新增讀鎖,不允許在同一讀寫鎖上新增寫鎖,而且讀寫鎖相互排斥,有了讀鎖的讀寫鎖不允許再新增寫鎖,有了寫鎖的讀寫鎖也不允許再新增讀鎖。在條件變數中,被條件變數阻塞的執行緒需要另一執行緒釋放訊號來喚醒。

為什麼需要執行緒同步


當一個執行緒被建立後,我們就失去了干涉該執行緒執行的權利。如果不加干涉地讓執行緒自由執行,可能會在某些情境下引發很多問題。以下是由於執行緒不同步而常見的問題:

資料競爭:當多個執行緒對同一共享資料進行讀寫操作時,由於併發執行的隨機性,會導致它們的操作出現衝突,從而導致資料的混亂與不一致。

死鎖:當多個執行緒同時競爭多個共享資源時,由於資源的有限性以及執行緒不合理的發生順序,會導致執行緒的相互等待,產生死鎖情況。

飢餓:當某些執行緒始終無法獲得對共享資源的訪問權時,就可能導致飢餓的情況,這些執行緒會一直處於等待狀態,無法執行下去。。

如何選擇同步機制


互斥鎖與自旋鎖在處理的場景上大致相同,不過由於其實現方式而引發了一些差異。互斥鎖無法獲得鎖時,會進入阻塞等待狀態,而自旋鎖會一直迴圈檢查所是否可用,而並不會讓執行緒進入阻塞狀態。在整個執行過程中,使用互斥鎖會在使用者態與核心態中切換,而自旋鎖只在使用者態中執行。

以上的差異導致了這兩種鎖在應用場景的差異。互斥鎖適合等待比較長的場景,因為執行緒在等待鎖的過程中會進入阻塞狀態,不會消耗CPU資源;而自旋鎖適用於縣城的等待時間比較短的情景,因為縣城在等待鎖的過程會一直檢查鎖是否可用,會消耗CPU資源。

在高併發的場景下,當多個執行緒在競爭同一個鎖,如果使用自旋鎖,自旋鎖會不斷詢問鎖的執行情況,佔用大量的CPU資源。相對而言,如果使用互斥鎖,當一個執行緒獲取鎖失敗時會進入阻塞狀態,放棄CPU的執行,直到該鎖可用並被喚醒後再繼續執行。因此,在高併發場景中,並且鎖的競爭比較激烈的時候,使用互斥鎖比使用自旋鎖更有效,因為這樣可以節省CPU資源,提高系統的併發效能。

讀寫鎖其實可以看做互斥鎖的一種特殊情景,我們對於資料或資源的使用無外乎讀取與修改,但是當一群執行緒對該資源或資料的使用僅限於正確的讀取,那麼使用互斥鎖就有些大動干戈了,我們只需要保證該資料或資源的一致性,而不必要求其它讀取執行緒的等待鎖的釋放。所以說,使用讀寫鎖可以提高系統在處理讀取任務時的效率。

以上三種鎖,雖然在實現與使用上有些差異,但是仍然可以把它們看做一個爹媽生的,不過條件變數就不同了。如果站在鎖的角度來衡量以上四種方法,在前三者方法透過用鎖制約其它執行緒,而自己也被鎖鎖限制,對於條件變數的訊號傳送執行緒來說,這波它站在大氣層,它就像遠端的管理著鎖的開閉,自己卻不會受到這把“鎖”的直接影響。以前覺得前三者在程式設計中只要不在乎效率,完全可以相互替代,那條件變數存在的意義是什麼呢,思考後我發現前三者中執行緒的關係是平等的,它們公平的去競爭;而條件變數中訊號的發起執行緒像是一個主宰,決定著其它執行緒的執行狀態。

競爭就好了?為什麼要專政呢?

用現實生活中的例子來看,如果一群人相互競爭,誰也不服誰,那他們註定不能變的井井有條,如果是單獨的任務還好,一旦遇上需要合作的情況,這時候就需要一個獨裁者來統籌規劃,來指明方向。再來用程式設計的角度來看,透過互斥的競爭手段可以保護共享資源,使得同一時間只有一個執行緒共享訪問資源,而條件變數可以用來協調執行緒的行為,它讓一個執行緒等待另一個執行緒的通知,從而協調執行緒執行的順序和進度。

以上的討論在大方向上給出了執行緒同步的用法,在使用的細節上也給大家提出些許建議。應該減少不必要的加鎖時間,我們在使用鎖的過程中主要有初始化、加鎖、解鎖和釋放鎖幾個步驟,在程式碼段中的加鎖與解鎖的位置會直接影響該執行緒擁有鎖的時間,從而對整體程式碼的效率造成影響,我們要避免將無關的程式碼放入程式碼塊中,從而縮短鎖的範圍和時間。

鎖要留給誰


我們透過程式設計可以決定鎖由誰加,不過當一個鎖被釋放時正好有多個執行緒在等待鎖,接下來鎖會分配給誰則是作業系統的排程演演算法來實現的。

類比眾多排程演演算法所考慮的那樣,都是在效率與公平間取得平衡,鎖的繼承也不例外。對鎖而言,公平性是指所有執行緒都有機會獲得鎖,而不是讓某些鎖永遠沒有得到鎖的機會;效率是指儘可能減少執行緒的等待時間。

比較常見的排程演演算法是先進先出排程,即按照執行緒等待鎖的先後順序,依次將鎖分配給等待時間最久的執行緒,另一種演演算法是將鎖分配給還沒有進入阻塞狀態的執行緒。前者可以保證執行緒使用鎖的公平性,後者則是透過減少阻塞與就緒態的切換來提高系統效率。

相關文章