《C++ concurrency in action》 讀書筆記 -- Part 4 第五章 C++的多線程內存模型 (1)


《C++ concurreny in action》 第五章 C++的內存模型和原子操作

5.1 Memory model basics (內在模型基礎)

Memory model 涉及兩個方面:structural 和 concurrency

structural 是基礎,主要是對象的布局

5.1.1 Objects and memory location

The C++ Standard defines an object as “a region of storage,”

clip_image001

注意四點:

  • 所有變量都有object,包括成員變量
  • 所有object都有自己的內存位置
  • 基礎類型(int之類的)都有自己單獨的內存區
  • bit field 共享同一個內存區

5.1.2 Objects, memory locations, and concurrency

產生race condition的條件就是多個線程訪問同一個memory location,同時至少有一個在修改這個memory location的值。

必須要控制訪問順序來避免race condition。兩種方法:1. 鎖(mutex)2. 原子操作(atomic operation)

5.1.3 Modification orders

數據的修改順序必須也有限制,否則會產生data race

5.2 Atomic operations and types in C++ (C++中的原子操作和類型)

An atomic operationis an indivisible operation.

原子操作就是不可分割的操作。要不就完成了, 要不就還沒有做。不可能出現“只做了一半”的狀態

在C++中,我們可以通過原子類型(atomic type)來進行原子操作。

5.2.1 Tthe standard atomic types

標准的原子類型都在頭文件<atomic>中。這里頭的類型的操作都是原子操作。

大多數都有 is_lock_free() 這個成員函數,如果返回 true ,則這個是“真正的原子操作(用的真正的原子操作指令)”,返回 false,則是使用鎖來模擬。

只有std::atomic_flag不帶有is_lock_free這個函數。因為這個類型必須是真正的原子操作。

其它的原子類型都是以std::atomic<>來實現的。

clip_image002

標准庫中的原子類型都是不可拷貝和賦值的(not copyable or assignable)

原子類型的操作函數中都有一個memory-ordering的參數選項,可以精確控制 memory-ordering 語義。但這相關的主要在5.3節詳述。

原子類型的操作分三類:

  • (存儲操作)Store operations, 有這幾個函數: memory_order_relaxed, memory_order_release, or memory_order_seq_cstordering
  • (Load操作?)Load operations,有這幾個函數: memory_order_relaxed, memory_order_consume, memory_order_acquire,or memory_order_seq_cstordering
  • (修改操作)Read-modify-write operations, 有這幾個函數:memory_order_relaxed, memory_

order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel,or memory_order_seq_cstordering

5.2.2 Operations on std::atomic_flag

std::atomic_flag 是標准庫中最簡單的原子類型,代表一個 bool 標志。這個類型的對象分兩種狀態:set 或者是 clear。這個類型的設計目的就是作為構建其它的原子類型,因此很少見它會被普通的程序使用。但因為很有代表性,所以本書從這個類型開始講起。

std::atomic_flag的對象必須用 ATOMIC_FLAG_INIT進行初始化,初始化后對象會處於clear狀態。(它是唯一一個對初始化有特殊要求的原子類型,但同時也是唯一一個會被保證是lock_free實現的原子類型)。靜態類型的std::atomic_flag也會由編譯器來保證初始化。

std::atomic_flag f=ATOMIC_FLAG_INIT;

一個被初始化后的std::atomic_flag對象可以做的三種操作有:

  • destroy(通過析構函數)
  • clear(通過 clear()函數)  store操作  參數可以指定memory-ordering tags,但是不能使用 memory_order_acquire or memory_order_acq_rel 這兩種語義
  • set並讀取狀態(通過test_and_set()函數)read_modify_write操作,可以使用任何memory-ordering tags。
f.clear(std::memory_order_release); 
bool x=f.test_and_set();

標准庫的原子類型的操作都是原子操作的,標准庫的原子類型都不帶拷貝和賦值,因為拷貝和賦值不可能是“原子操作”(涉及兩個對象)。

因為本身只有有限幾個關鍵的操作,所以std::atomic_flag很適合用在實現自旋鎖。

class spinlock_mutex
{
    std::atomic_flag flag;
public:
    spinlock_mutex():flag(ATOMIC_FLAG_INIT)
    { }
    void lock()
    {
        while(flag.test_and_set(std::memory_order_acquire));
    }
    void unlock()
    {
        flag.clear(std::memory_order_release);
    }
};

這個實現非常簡陋,但已經可以足夠用在std::lock_guard<>上,作為互斥鎖來使用了。

std::atomic_flag的操作實在太有限,因此無法作為一個通用的bool標志來使用(因為沒有一個單純做值讀取的操作)。如果需要通用的bool標志,那么應該使用 std::atomic<bool>。

5.2.3 Operations on std::atomic<bool>

std::atomic<boo>可以通過一個bool值來構建

std::atomic<bool> b(true);
b=false;

std::atomic類型的賦值都是(非atomic類型的)返回值而不是引用。比如(std::atomic<int>的賦值操作返回的是int而不是int&),以避免在別的線程獲取這個引用並通過非原子操作來修改它。

與std::atomic_flag不同,std::atomic<boo>通過以下幾個方法來操作:

  • store()  => 賦值,可以指定memory_older
  • load() => 取得原子類型對象的值
  • exchange() => read_modify_write操作。

下面是代表示例:

std::atomic<bool> b;
bool x=b.load(std::memory_order_acquire);
b.store(true);
x=b.exchange(false,std::memory_order_acq_rel);

std::atomic<boo>還有一些別的read_modify_write操作:

compare_exchange_weak( T& expected, T desired, ……)和compare_exchange_strong(T& expected, T desired, ……)

這兩個函數我們現在只關注前兩個參數(所以后面打了省略號,注意,這兩個函數不是“可變參數函數哦~”),功能是這樣:如果對象的值和expected一樣,那么,就賦值成desired。而如果對象的值與expected不一樣,則把expected的值賦值為現在對象的值。

(我:其實用std::atomic<bool>作為例子來講這兩個參數稍微有點點晦澀,用int的話好理解多了)

這兩個函數的返回值都是bool類型,true代表store的操作進行了,false則沒有進行。他們的區別在於:compare_exchange_weak可能對象值與expected一致,函數也可能會返回false,因為把desired賦值給對象會失敗(特別是對於沒有compare/exchange指令的CPU),失敗的情況下不會更新std::atomic對象的值,compare_exchange_strong返回false則表示對象值與expected是不同的。

(這兩個參數還可以指定memory_older,說實話,現在我基本上沒看明白,還是看完5.3再回來消化吧。)

5.2.4 Operations on std::atomic<T*>: pointer arithmetic

指向一個T對象的指針的原子類型。基本上和std::atomic<bool>,有着上面介紹的所有操作。但多出了一些“指針運算操作”。fetch_add()和fetch_sub(),就“前進”和“后退”相應的距離。與有+=,-=和前、后綴的++和--。注意的是fetch_xx返回的是原來的值(而不是運算后的值)。

而+=,-=,++,--等的語義則與我們平常使用的指針是完全一致的。

class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x=p.fetch_add(2); 
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1); 
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);

5.2.5 Operation on standard atomic integral types

其它的整形的原子類型的操作基本上就比較相同了,放在這一節進行一個概述:(load(),  store(),  exchange(),  compare_exchange_weak(), and compare_exchange_strong())之類上面介紹的操作當然都是有的。也有像:fetch_add(), fetch_sub(), fetch_and(), fetch_or(),
fetch_xor()這樣的操作,分別代表了:(+=, -=, &=, |=, and ^=),還有前后綴的--,++。但沒有乘,除和位運算。但由於原子類型一般主要用來計數,所以我們不會感覺到太多不便,實在需要的時候也可以使用compare_exchange_weak()加循環來得到。

5.2.6 The std::atomic<> primary class template

可以用atomic<>來做自定義的原子類型,但對於放入的模板參數有比較多的限制,我們可以這么認為:可以接受用淺拷貝和按位對比(bitwise compare)的類型才能作為atomic<T>中的T。(具體的說明請看原文)

5.2.7 Free functions for atomic operations

上面介紹的都是std::atomic的成員函數,其實它們都有相應對的free函數版本,支持情況如下表:

image

free函數設計得更為C一些,因此引用被換成了指針。

另外,C++標准庫為std::shared_ptr提供了一些重要的輔助函數,讓這些智能指針可以以“原子操作”的方式獲取值,設置值。

std::shared_ptr<my_data> p;
void process_global_data()
{
    std::shared_ptr<my_data> local=std::atomic_load(&p);
    process_data(local);
}
void update_global_data()
{
    std::shared_ptr<my_data> local(new my_data);
    std::atomic_store(&p,local);
}

它們都是以std::shared_ptr<>*作為第一個參數的,主要有:load,store,exchange和compare/exchange。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM