C 效能之無鎖程式設計

2022-09-06 14:48:20 字數 2570 閱讀 3891

私以為個人的技術水平應該是乙個螺旋式上公升的過程:先從書本去了解乙個大概,然後在實踐中加深對相關知識的理解,遇到問題後再次回到書本,然後繼續實踐……接觸c++併發程式設計已經一年多,從慢慢啃《c++併發程式設計實戰》這本書開始,不停在**高頻交易軟體的開發實踐中去理解、運用、優化多執行緒相關技術。多執行緒知識的學習也是先從最基本的執行緒建立、互斥鎖、條件變數到更高階的執行緒安全資料結構、執行緒池等等技術,當然在專案中也用到了簡單的無鎖程式設計相關知識,今天把一些體會心得跟大家分享一下,如有錯誤,還望大家批評指正。

在編寫多執行緒程式時,最重要的問題就是多執行緒間共享資料的保護。多個執行緒之間共享位址空間,所以多個執行緒共享程序中的全域性變數和堆,都可以對全域性變數和堆上的資料進行讀寫,但是如果兩個執行緒同時修改同乙個資料,可能造成某執行緒的修改丟失;如果乙個執行緒寫的同時,另乙個執行緒去讀該資料時可能會讀到寫了一半的資料。這些行為都是執行緒不安全的行為,會造成程式執行邏輯出現錯誤。舉個最簡單的例子:

#include #include using namespace std;

int i = 0;

mutex mut;

void iplusplus()

}int main()

上面**main函式中建立了兩個執行緒thread1和thread2,兩個執行緒都是執行iplusplus函式,該函式功能就是執行i++語句10000000次,按照常識,兩個執行緒各對i自增10000000次,最後i的結果應該是20000000,但是執行後結果卻是

i並不等於20000000,這是在多執行緒讀寫情況下沒有對執行緒間共享的變數i進行保護所導致的問題。

對於保護多執行緒共享資料,最常用也是最基本的方法就是使用c++11執行緒標準庫提供的互斥鎖mutex保護臨界區,保證同一時間只能有乙個執行緒可以獲取鎖,持有鎖的執行緒可以對共享變數進行修改,修改完畢後釋放鎖,而不持有鎖的執行緒阻塞等待直到獲取到鎖,然後才能對共享變數進行修改,這種方法幾乎是併發程式設計中的標準做法。大體流程如下:

#include 

#include

#include

#include

#include

using namespace std;

int i = 0;

mutex mut; //互斥鎖

void iplusplus()

}int main()

**14行和16行分別為互斥鎖加鎖和解鎖**,29行我們列印程式執行耗時,**執行結果

可以看到,通過加互斥鎖,i的執行結果是正確的,由此解決了多執行緒同時寫乙個資料產生的執行緒安全問題,**總耗時3.37328ms。

原子操作是無鎖程式設計的基石,原子操作是不可分隔的操作,一般通過cas(compare and swap)操作實現,cas操作需要輸入兩個數值,乙個舊值(期望操作前的值)和乙個新值,在操作期間先比較下舊值有沒有發生變化,如果沒有發生變化,才交換成新值,發生了變化則不交換。c++11的執行緒庫為我們提供了一系列原子型別,同時提供了相對應的原子操作,我們通過使用這些原子型別即可擺脫每次對共享變數進行操作都進行的加鎖解鎖動作,節省了系統開銷,同時避免了執行緒因阻塞而頻繁的切換。原子型別的基本使用方法如下:

#include #include #include #include #include using namespace std;

atomici = 0;

void iplusplus()

}int main()

**的第8行定義了乙個原子型別(int)變數i,在第13行多執行緒修改i的時候即可免去加鎖和解鎖的步驟,同時又能保證變數i的執行緒安全性。**執行結果如下:

可以看到i的值是符合預期的,**執行總耗時1.12731ms,僅為有鎖程式設計的耗時3.37328ms的1/3,由此可以看出無鎖程式設計由於避免了加鎖而相對於有鎖程式設計提高了一定的效能。

無鎖程式設計最大的優勢是什麼?是效能提高嗎?其實並不是,我們的測試**中臨界區非常短,只有乙個語句,所以顯得加鎖解鎖操作對程式效能影響很大,但在實際應用中,我們的臨界區一般不會這麼短,臨界區越長,加鎖和解鎖操作的效能損耗越微小,無鎖程式設計和有鎖程式設計之間的效能差距也就越微小。

我認為無鎖程式設計最大的優勢在於兩點:

避免了死鎖的產生。由於無鎖程式設計避免了使用鎖,所以也就不會出現併發程式設計中最讓人頭疼的死鎖問題,對於提高程式健壯性有很大積極意義

**更加清晰與簡潔。對於乙個多執行緒共享的變數,保證其安全性我們只需在宣告時將其宣告為原子型別即可,在**中使用的時候和使用乙個普通變數一樣,而不用每次使用都要在前面寫個加鎖操作,在後面寫乙個解鎖操作。我寫的c++**高頻交易軟體中,有乙個全域性變數fund,儲存的是當前資金量,程式採用執行緒池執行交易策略,交易策略中頻繁使用到fund變數,如果採用加鎖的方式,使用起來極其繁瑣,為了保護乙個fund變數需要非常頻繁的加鎖解鎖,後來將fund變數改為原子型別,後面使用就不用再考慮加鎖問題,整個程式閱讀起來清晰很多。

如果是為了提高效能將程式大幅改寫成無鎖程式設計,一般來說結果可能會讓我們失望,而且無鎖程式設計裡面需要注意的地方也非常多,比如aba問題,記憶體順序問題,正確實現無鎖程式設計比實現有鎖程式設計要困難很多,除非有必要(確定了效能瓶頸)才去考慮使用無鎖程式設計,否則還是使用互斥鎖更好,畢竟程式的高效能是建立在程式正確性的基礎上,如果程式不正確,一切效能提公升都是徒勞無功。

原文 

效能調優基礎篇之無鎖實現cache更新

業務需求 乙個執行緒寫,個執行緒讀 為了提高在高併發情況下的查詢效率,應用在記憶體中維護了乙份cache,cache內容需訪問 實時更新.更新cache,無非批量更新與增量更新這兩種空間與時間的權衡。增量更新過於複雜,這裡採用了批量更新的方式。當然最重要的是要保證cache執行緒安全。最容易想到的方...

多執行緒無鎖演算法之無鎖佇列的實現

今天花了近兩個小時的時間好好的理解了一下多執行緒無鎖佇列的實現,檢視了很多資料和文獻。在我看來,實現無鎖佇列的關鍵點有二 1 各個平台的原子操作或者說cas原語 2 aba問題的理解和解決。首先來說說這個cas原語,所謂cas compare and swap 即比較並交換,在 intel 處理器中...

多執行緒無鎖演算法之無鎖佇列的實現

今天花了近兩個小時的時間好好的理解了一下多執行緒無鎖佇列的實現,檢視了很多資料和文獻。在我看來,實現無鎖佇列的關鍵點有二 1 各個平台的原子操作或者說cas原語 2 aba問題的理解和解決。首先來說說這個cas原語,所謂cas compare and swap 即比較並交換,在 intel 處理器中...