1. 程式人生 > 其它 >C++11多執行緒(std::atomic)

C++11多執行緒(std::atomic)

在化學中原子不是可分割的最小單位,引申到程式設計中,原子操作是不可打斷的最低粒度操作,是執行緒安全的。C++11中原子類提供的成員函式都是原子的,是執行緒安全的。

1. std::atomic_flag

C++11中所有的原子類都是不允許拷貝、不允許Move的,atomic_flag也不例外。atomic_flag顧名思議,提供了標誌的管理,標誌有三種狀態:clear、set和未初始化狀態。

1.1 atomic_flag例項化

預設情況下atomic_flag處於未初始化狀態。除非初始化時使用了ATOMIC_FLAG_INIT巨集,則此時atomic_flag處於clear狀態。

1.2 std::atomic_flag::clear

呼叫該函式將會把atomic_flag置為clear狀態。clear狀態您可以理解為bool型別的false,set狀態可理解為true狀態。clear函式沒有任何返回值:

void clear(memory_order m = memory_order_seq_cst) volatile noexcept;
void clear(memory_order m = memory_order_seq_cst) noexcept;

對於memory_order我們會在後面的章節中詳細介紹它,現在先列出其取值及簡單釋義

序號意義
1 memory_order_relaxed 寬鬆模型,不對執行順序做保證
2 memory_order_consume 當前執行緒中,滿足happens-before原則。
當前執行緒中該原子的所有後續操作,必須在本條操作完成之後執行
3 memory_order_acquire 當前執行緒中,操作滿足happens-before原則。
所有後續的操作必須在本操作完成後執行
4 memory_order_release 當前執行緒中,操作滿足happens-before原則。
所有後續的操作必須在本操作完成後執行
5 memory_order_acq_rel 當前執行緒中,同時滿足memory_order_acquire和memory_order_release
6 memory_order_seq_cst 最強約束。全部讀寫都按順序執行

1.3 test_and_set

該函式會檢測flag是否處於set狀態,如果不是,則將其設定為set狀態,並返回false;否則返回true。

test_and_set是典型的read-modify-write(RMW)模型,保證多執行緒環境下只被設定一次。下面程式碼通過10個執行緒,模擬了一個計數程式,第一個完成計數的會列印"win"。
#include <atomic>
#include <iostream>
#include <list>
#include <thread>

void race(std::atomic_flag &af, int id, int n)
{
for (int i = 0; i < n; i++);
// 第一個完成計數的列印:Win if (!af.test_and_set())
  { printf(
"%s[%d] win!!!\n", __FUNCTION__, id);   } } int main()
{ std::atomic_flag af
= ATOMIC_FLAG_INIT; std::list<std::thread> lstThread; for (int i = 0; i < 10; i++)
  { lstThread.emplace_back(race, std::
ref(af), i + 1, 5000 * 10000);   } for (std::thread &thr : lstThread)
  { thr.join(); }
return 0; }

程式輸出如下(每次執行,可能率先完成的thread不同):

race[7] win!!!

2. std::atomic<T>

std::atomic是一個模板類,它定義了一些atomic應該具有的通用操作,我們一起來看一下:

2.1 is_lock_free

bool is_lock_free() const noexcept;
bool is_lock_free() const volatile noexcept;
atomic是否無鎖操作。如果是,則在多個執行緒訪問該物件時不會導致執行緒阻塞(可能使用某種事務記憶體transactional memory方法實現lock-free的特性)。
事實上該函式可以做為一個靜態函式。所有指定相同型別T的atomic例項的is_lock_free函式都會返回相同值。

2.2 store

void store(T desr, memory_order m = memory_order_seq_cst) noexcept;
void store(T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
T operator=(T d) noexcept;
T operator=(T d) volatile noexcept;

賦值操作。operator=實際上內部呼叫了store,並返回d。

T operator=(T d) volatile noexpect {
    store(d);
    return d;
}

注:有些編譯器,在實現store時限定m只能取以下三個值:memory_order_consume,memory_order_acquire,memory_order_acq_rel。

2.3 load

T load(memory_order m = memory_order_seq_cst) const volatile noexcept;
T load(memory_order m = memory_order_seq_cst) const noexcept;
operator T() const volatile noexcept;
operator T() const noexcept;

讀取,載入並返回變數的值。operator T是load的簡化版,內部呼叫的是load(memory_order_seq_cst)形式。

2.4 exchange

T exchange(T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
T exchange(T desr, memory_order m = memory_order_seq_cst) noexcept;

交換,賦值後返回變數賦值前的值。exchange也稱為read-modify-write操作。

2.5 compare_exchange_weak

bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) volatile noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order s, memory_order f) noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak(T& expect, T desr, memory_order m = memory_order_seq_cst) noexcept;

這就是有名的CAS(Compare And Swap: 比較並交換)。但C++11針對該操作提供了更多的細節,其操作流程如下:

以上只是個示意圖,compare_exchange_weak操作是原子的,排它的。其它執行緒如果想要讀取或修改該原子物件時,會等待先該操作完成。
該函式直接比較原子物件所封裝的值與expect的物理內容,在某些情況下,物件的比較操作在使用 operator==() 判斷時相等,但 compare_exchange_weak 判斷時卻可能失敗,因為物件底層的物理內容中可能存在位對齊或其他邏輯表示相同但是物理表示不同的值(比如 true 和 5,它們在邏輯上都表示"真",但在物理上兩者的表示並不相同)。
與strong版本不同,weak版允許返回偽false,即使原子物件所封裝的值與expect的物理內容相同,也仍然返回false。但它在某些平臺下會取得更好的效能,在某些迴圈演算法中這種行為也是可接受的。對於非迴圈演算法建議使用compare_exchange_strong。

2.6 compare_exchange_strong

bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) volatile noexcept;
bool compare_exchange_strong(T& expect, T desr, memory_order s, memory_order f) noexcept;
bool compare_exchange_strong(T& expect, T desr, memory_order m = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong(T& expc, T desr, memory_order m = memory_order_seq_cst) noexcept;

compare_exchange的strong版本,進行compare時,與weak版一樣,都是比較的物理內容。與weak版不同的是,strong版本不會返回偽false。即:原子物件所封裝的值如果與expect在物理內容上相同,strong版本一定會返回true。其所付出的代價是:在某些需要迴圈檢測的演算法,或某些平臺下,其效能較compare_exchange_weak要差。但對於某些不需要採用迴圈檢測的演算法而言, 通常採用compare_exchange_strong 更好。

3. std::atomic特化

我知道計算擅長處理整數以及指標,並且X86架構的CPU還提供了指令級的CAS操作。C++11為了充分發揮計算的特長,針對非浮點數值(std::atmoic<integral>)及指標(std::atomic<T*>)進行了特化,以提高原子操作的效能。特化後的atomic在通用操作的基礎上,還提供了更豐富的功能。

3.1 fetch_add

// T is integral
T fetch_add(T v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_add(T v, memory_order m = memory_order_seq_cst) noexcept;
// T is pointer
T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_add(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept;

該函式將原子物件封裝的值加上v,同時返回原子物件的舊值。其功能用偽程式碼表示為:

auto old = contained
contained += v
return old

其中contained為原子物件封裝值,本文後面均使用contained代表該值。注:以上是為了便於理解的虛擬碼,實際實現是原子的不可拆分的。

3.2 fetch_sub

// T is integral
T fetch_sub(T v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_sub(T v, memory_order m = memory_order_seq_cst) noexcept;
// T is pointer
T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) volatile noexcept;
T fetch_sub(ptrdiff_t v, memory_order m = memory_order_seq_cst) noexcept;

該函式將原子物件封裝的值減去v,同時返回原子物件的舊值。其功能用偽程式碼表示為:

auto old = contained
contained -= v
return old

3.3 ++, --, +=, -=

不管是基於整數的特化,還是指標特化,atomic均支援這四種操作。其用法與未封裝時一樣,此處就不一一列舉其函式原型了。

4. 獨屬於數值型特化的原子操作 - 位操作

4.1 fetch_and,fetch_or,fetch_xor

位操作,將contained按指定方式進行位操作,並返回contained的舊值。

integral fetch_and(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_and(integral v, memory_order m = memory_order_seq_cst) noexcept;
integral fetch_or(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_or(integral v, memory_order m = memory_order_seq_cst) noexcept;
integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) volatile noexcept;
integral fetch_xor(integral v, memory_order m = memory_order_seq_cst) noexcept;

以xor為例,其操作相當於

auto old = contained
contained ^= v
return old

4.2 operator &=,operator |=,operator ^=

與相應的fetch_*操作不同的是,operator操作返回的是新值:

T operator &=(T v) volatile noexcept {return fetch_and(v) & v;}
T operator &=(T v) noexcept {return fetch_and(v) & v;}
T operator |=(T v) volatile noexcept {return fetch_or(v) | v;}
T operator |=(T v) noexcept {return fetch_or(v) | v;}
T operator ^=(T v) volatile noexcept {return fetch_xor(v) ^ v;}
T operator ^=(T v) noexcept {return fetch_xor(v) ^ v;}

5. std::atomic的限制:trivially copyable

上面我們提到std::atomic提供了通用操作,其實這些操作可以應用到所有trivially copyable的型別。trivially copyable在cppreference中文站被譯為“可平凡複製”。網上也有人譯作拷貝不變。一個型別如果是trivially copyable,則使用memcpy這種方式把它的資料從一個地方拷貝出來會得到相同的結果。因此本文使用拷貝不變這個中文翻譯,請大家不要糾結中文翻譯,明白本文所表達的意思即可。編譯器如何判斷一個型別是否trivially copyable呢?C++標準把trivial型別定義如下,一個拷貝不變(trivially copyable)型別是指:

  1. 沒有non-trivial 的拷貝建構函式
  2. 沒有non-trivial的move建構函式
  3. 沒有non-trivial的賦值操作符
  4. 沒有non-trivial的move賦值操作符
  5. 有一個trivial的解構函式

一個trivial class型別是指有一個trivial型別的預設建構函式,而且是拷貝不變的(trivially copyable)的class。特別注意,拷貝不變型別和trivial型別都不能有虛機制。那麼trivial和non-trivial型別到底是什麼呢?這裡給出一個非官方、不嚴謹的判斷方式,方便大家對trivially copyable有一個直觀的認識。一個trivial copyable類在四個點上沒有自定義動作,也沒有編譯器加入的額外動作(如虛指標初始化就屬額外動作),這四個點是:

  • 預設構造。類必須支援預設構造,同時類的非靜態成員也不能有自定義或編譯器加入的額外動作,否則編譯器勢必會隱式插入額外動作來初始化非靜態成員。
  • 拷貝構造、拷貝賦值
  • move構造、move賦值
  • 析構

為了加深理解,我們來看一下下面的例子(所有的類都是trivial的):

// 空類
struct A1 {};

// 成員變數是trivial的
struct A2 {
    int x;
};

// 基類是trivial的
struct A3 : A2 {
    // 非使用者自定義的建構函式(使用編譯器提供的default構造)
    A3() = default;
    int y;
};

struct A4 {
    int a;
private: // 對防問限定符沒有要求,A4仍然是trivial的
    int b;
};

struct A5 {
    A1 a;
    A2 b;
    A3 c;
    A4 d;
};

struct A6 {
    A2 a[16];
};

struct A7 {
    A6 c;
    void f(); // 普通成員函式是允許的
};

struct A8 {
     int x;
    // 對靜態成員無要求(std::string是non-trivial的)
     static std::string y;
};

struct A9 {
    // 非使用者自定義
    A9() = default;
    // 普通建構函式是可以的(前提是我們已經有了非定義的預設建構函式)
    A9(int x) : x(x) {};
    int x;
};

而下面這些型別都是non-trivial的

struct B {
    // 有虛擬函式(編譯器會隱式生成預設構造,同時會初始化虛擬函式指標)
    virtual f();
};

struct B2 {
    // 使用者自定義預設建構函式
    B2() : z(42) {}
    int z;
};

struct B3 {
    B3();
    int w;
};
// 雖然使用了default,但在預設構造宣告處未指定,因此被判斷為non-trivial的
NonTrivial3::NonTrivial3() = default;

struct B4 {
   // 虛析構是non-trivial的
    virtual ~B4();
};

STL在其標頭檔案<type_traits>中定義了對trivially copyable型別的檢測:

template <typename T>
struct std::is_trivially_copyable;
判斷類A是否trivially copyable:std::is_trivially_copyable<A>::value,該值是一個const bool型別,如果為true則是trivially copyable的,否則不是。

轉載於:C++11多執行緒-原子操作(1) - 簡書 (jianshu.com) C++11多執行緒-原子操作(2) - 簡書 (jianshu.com)