《深入理解C 11》筆記 原子型別和原子操作

2021-08-21 13:56:32 字數 4545 閱讀 5796

原子操作就是在多執行緒程式中」最小的且不可並行化的」操作,意味著多個執行緒訪問同乙個資源時,有且僅有乙個執行緒能對資源進行操作。通常情況下原子操作可以通過互斥的訪問方式來保證,例如linux下的互斥鎖,windows下的臨界區等。下面讓我們看個例子:

long

long total = 0;

void func()

}int main(void)

上面的**,兩個執行緒同時對total進行操作,並且沒有做互斥來保證同步。如果做了互斥,也就是total不會被兩個執行緒同時操作,那麼結果會等於9999999900000000。但是因為沒有做互斥,就會出現兩個執行緒同時讀取了暫存器中的值,分別操作之後又寫入暫存器,這樣就會有乙個執行緒的增加操作無效,所以得出的結果就會小於9999999900000000,且結果未知。在c++11以前,我們可以通過互斥來保證兩個執行緒不會同時操作total,例如:

long

long total = 0;

mutex total_mutex; // 偽**

void func()

}int main(void)

這樣能保證執行緒間的同步,但是如果資源需要在多個地方進行操作,就需要頻繁的lock和unlock,所以在c++11中新增了原子型別,讓**更加簡潔:

std::atomic_llong total = 0;            // atomic_llong相當於long long,但是本身就擁有原子性

void func()

}int main(void)

可以看到,使用了原子型別atomic_llong之後,不需要使用額外的互斥介面來保證total的同步,total自身就能保證。除了atomic_llong型別,c++11還提供了其他的原子型別。

當我們去看這些型別的定義時會發現,起始它們都是用atomic模板來定義的。例如std::atomic_llong就是用std::atomic來定義的。

原子型別支援的原子操作

c++11中將原子操作定義為atomic模板類的成員函式,包括了大多數型別的操作,比如讀寫、交換等。對於內建型別,主要通過過載全域性操作符來實現。下面列出所有atomic型別及其支援的相關操作列表:

列表中的atomic-intergral-type以及atomic就是前面的原子型別列表中的型別,class-type是自定義型別。對於大部分原子型別,都支援讀(load)、寫(store)、交換(exchange)等操作:

std:

:atomic n = 0;

int a = n; // 相當於int a = n.load();

n = 1; // 相當於a.store(1);

int b = n.exchange(1); // n賦值為1,返回n原來的值

另外,列表中有乙個比較特殊的atomic_flag型別,atomic_flag與其他型別不同,它是無鎖(lock_free)的,而其他的型別不一定是無鎖的。因為,atomic並不能保證型別t是無鎖的,另外不同平台的處理器處理方式不同,也不能保證必定無鎖,所以其他的型別都會有is_lock_free來判斷是否是無鎖的。atomic_flag只支援test_and_set以及clear兩個成員函式,test_and_set函式檢查 std::atomic_flag 標誌,如果 std::atomic_flag 之前沒有被設定過,則設定 std::atomic_flag 的標誌,並返回先前該 std::atomic_flag 物件是否被設定過,如果之前 std::atomic_flag 物件已被設定,則返回 true,否則返回 false;clear函式清除 std::atomic_flag 標誌使得下一次呼叫 std::atomic_flag::test_and_set 返回 false。可以用這兩個函式來實現乙個自旋鎖:

void func1()

std::cout

<< "do something"

<< std::endl;

}void func2()

int main(void)

以上**中,執行緒t1呼叫test_and_set一直返回true(因為在主線程中被設定過),所以一直在等待,而等待一段時間後當執行緒t2執行並呼叫了clear,test_and_set返回了false退出迴圈等待並進行相應操作。這樣一來,就實現了乙個執行緒等待另乙個執行緒的效果。

記憶體模型、順序一致性和memory_order

了解這一小節的內容之前,先看一段**:

void func1()

void func2()

int main(void)

不過在c++11中順序一致性只是多種記憶體模型中的一種,**並非必須按照順序執行,因為順序往往意味著最低效的同步方式。在了解其他記憶體模型之前,我們需要先了解一些處理器和編譯器相關的知識。記憶體模型通常是硬體上的概念,表示的是機器指令是以什麼樣的順序被處理器執行的,現代的處理器並不是逐條處理機器指令的:

1: load    reg3, 1;           // 將立即數1放入暫存器reg3

2: move reg4,reg3; // 將reg3的資料放入reg4

3: store reg4, a; // 將reg4的資料存入記憶體位址a

4: load reg5, 2; // 將立即數2放入暫存器reg5

5: store reg5, b; // 將reg5的資料存入記憶體位址b

以上的偽彙編**代表了temp = 1; a = temp; b = 2,通常情況下指令都是按照1~5的順序執行,這種記憶體模型稱為強順序(strong ordered)。不過可以看到,指令1、2、3和指令4、5的執行順序不影響結果,有一些處理器可能會將指令的順序打亂,例如按照1-4-2-5-3的順序執行,這種記憶體模型稱為弱順序(weak ordered)。

在多執行緒程式中,強順序型別意味著對於各個執行緒看到的指令執行順序是一致的。對於處理器而言,看到記憶體中的資料被改變的順序與機器指定中的一致。相反的,弱順序就是各個執行緒看到的記憶體資料被改變的順序與機器指定中宣告的不一致。如果乙個平台是弱順序記憶體模型,那麼上文列印a,b的**中,執行緒func2就有可能列印出0,2的結果。既然弱順序記憶體模型可能會導致程式問題,為什麼有些平台會使用這種模型?簡單的說,這種模型能讓處理器有更好的並行性,提高指令執行的效率。並且,彙編指令中有一條記憶體柵欄指令,能保證指令的順序執行,但是會影響處理器效能。

介紹了硬體記憶體模型後,再來說說c++11中定義的記憶體模型和順序一致性和硬體中的關係。高階語言和機器指令是通過編譯器來進行轉換的,而編譯器處於**優化的考慮,會將指令進行移動。對於c++11的記憶體模型而言,要保證**的順序一致性,需要同時做到以下幾點:

對於上文列印a,b的**來說,如果只需要在主線程中列印結果,那麼**的執行順序並不重要。但是atomic原子型別預設的順序一致性會要求編譯器禁用優化,這無疑增加了效能開銷。於是c++11中,設計了能夠對原子型別指定記憶體順序memory_order。我們把上文列印a,b的**中的func1做一下修改:

void func1()

上面的**使用了store函式進行賦值,store函式接受兩個引數,第乙個是要寫入的值,第二個是名為memory_order的列舉值。這裡使用了std::memory_order_relaxed,表示鬆散記憶體順序,該列舉值代表編譯器可以任由編譯器重新排序或則由處理器亂序處理。這樣a和b的賦值執行順序性就被解除了,對於func2中的列印語句,列印出0,2的結果也就是合理的了。在c++11中一共有7種memory_order列舉值,預設按照memory_order_seq_cst執行:

需要注意的是,不是所有的memory_order都能被atomic成員使用:

原子型別提供的一些操作符都是memory_order_seq_cst的封裝,所以他們都是順序一致性的。

最後說明一下,std::atomic和std::memory_order只有在多cpu多執行緒情況下,無鎖程式設計才會用到。在x86下,由於是strong memory order的,所以很多時候只需要考慮編譯器優化;保險起見,可以用std::atomic,他會同時處理編譯器優化和cpu的memory order(雖然x86用不到)。但是在除非必要的情況下,不用使用std::memory_order,std::atmoic預設用的是最強限制。

深入理解C 11 筆記

include using namespace std classa a 對於含有堆記憶體的類,需要提供深拷貝的拷貝建構函式,避免預設的拷貝構造使用淺拷貝導致堆記憶體的重複刪除。a const a a m ptr new int a.m ptr 通過移動構造,a 作為函式引數,只使用淺拷貝避免臨時物...

《深入理解C 11》筆記 decltype

本篇將介紹decltype的用法。decltype與auto類似,也能進行型別推導,但是用法有一定的區別,decltype推導出的型別能作為型別宣告變數 int main decltype的應用 一種是decltype和typedef using的合用 using size t decltype s...

《深入理解C 11》筆記 追蹤返回型別

templatedecltype 2 a doublevalue t a 用decltype推導返回型別但是對於編譯器來說,是從左到右進行編譯的,decltype在進行推導時並不知道a的型別,所以這種寫法是編譯不過的。為了解決這個問題,於是引入了追蹤返回型別 template auto double...