日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
關(guān)于多線程的一切:原子操作


接上篇《??關(guān)于多線程同步的一切:偽共享??》

原子,意味著不可切分的最小單元,程序中的原子操作指任務(wù)不可切分到更小的步驟。

原子性(atomic)是一個可見性的概念:

當我們稱一個操作是atomic的,實際上隱含了一個對什么atomic的上下文。

注意:我們說的是從線程視角觀察不到完成一半的狀態(tài),而并非不存在物理上的進度狀態(tài),它取決于你的觀察視角。

比如說一個線程中被互斥鎖保護的區(qū)域,對另一個線程是atomic的,因為從另一個線程視角來看,它沒法進入臨界區(qū)讀到數(shù)據(jù)中間狀態(tài),但是對kernel而言卻不是atomic的。

從線程視角只能觀察到未做和已做兩種狀態(tài),觀察不到完成一半的狀態(tài),任務(wù)執(zhí)行不會被中斷,也不會穿插進其他操作。

原子性對多線程操作是一個非常重要的屬性,因為它不可切分,所以,一個線程沒法在另一個線程執(zhí)行原子操作的時候穿插進去。

比如一個線程原子的寫入共享數(shù)據(jù),那么其他線程沒有辦法讀到“半修改的數(shù)據(jù)”;同樣,如果一個線程原子讀取共享數(shù)據(jù),那么它讀取的是共享變量在那個瞬間的值,因此原子的讀和寫沒有數(shù)據(jù)競爭(Data Race)。

原子操作常用于與順序無關(guān)的場景。

原子指令

原子指令指單一的不可再分的不可中斷的被硬件直接執(zhí)行的機器指令,原子指令是無鎖編程的基石。

原子指令常被分成兩類:

  • store/load
  • read-modify-write(RMW)

Store/Load指令

  • store:存儲數(shù)據(jù)到內(nèi)存,對應(yīng)變量寫(賦值)
  • load:從內(nèi)存加載數(shù)據(jù),對應(yīng)變量讀

通常,一條簡單的store/load機器指令是原子的,比如數(shù)據(jù)復(fù)制指令(mov)可以把內(nèi)存位置的數(shù)據(jù)讀取到CPU寄存器,相當于Load數(shù)據(jù)。

x86架構(gòu)讀/寫“按數(shù)據(jù)類型對齊要求對齊的長度不大于機器字長的數(shù)據(jù)”是原子的。

那什么是數(shù)據(jù)類型對齊要求呢?

比如在x86_64架構(gòu)LLP64系統(tǒng)上(LLP64指long、long long和pointer類型是64位的),只要int32類型數(shù)據(jù)滿足放置在起始地址除4為0,int64/long類型數(shù)據(jù)滿足起始地址除8為0,則該數(shù)據(jù)就是滿足類型對齊要求,那么對它的讀和寫,都是原子的。

一字節(jié)的數(shù)據(jù)讀寫一定是原子的。

其實,Intel新CPU架構(gòu)確保讀寫放置在一個Cache Line的數(shù)據(jù)(不大于機器字長),跨Cache Line的數(shù)據(jù)訪問無法保證原子性。

C/C++編程中,變量和結(jié)構(gòu)體會自動滿足對齊要求,比如:

int i;

void f() {
long y;
}

struct Foo {
int x;
short s;
void* ptr;
} foo;

全局變量i會被放置在起始地址可以被4整除的內(nèi)存位置,局部變量y會被放置在起始地址可以被8整除的內(nèi)存位置,而結(jié)構(gòu)體內(nèi)的x成員會被放置在起始地址可以被4整除的內(nèi)存位置。

為了把ptr安置在起始地址可以被8整除的內(nèi)存位置,編譯器會在s后加入填充,從而使得ptr也滿足對齊要求。

通過C malloc()接口動態(tài)分配的內(nèi)存,其返回值一般也會對齊到8/16字節(jié),如果有更高的內(nèi)存對齊要求,可以通過aligned_alloc(alignment, size)接口。C++中的alignas關(guān)鍵字用于設(shè)置結(jié)構(gòu)或變量的對齊要求。

對一個滿足對齊要求的不大于機器字長的類型變量賦值是原子的,不會出現(xiàn)半完成(即只完成一半字節(jié)的賦值),讀的情況亦如此。

注意:對長度大于機器字長的數(shù)據(jù)讀寫,是不符合原子操作特征的,比如在x86_64系統(tǒng)上,對下面結(jié)構(gòu)體變量的讀寫都是非原子的:

struct Foo {
int a;
int b;
int c;
} foo1;

void set_foo1(const Foo& f) {
foo1 = f;
}

foo1包含3個int成員共12字節(jié),大于機器字長8字節(jié),所以對`foo1 = f`不是原子的。

基于以上知識,我們便知道,一些getter/setter接口,即使在多線程環(huán)境下,也可以不用加鎖,比如:

struct Foo {
size_t get_x() const { // OK
return x;
}

void set_y(float y) { // OK
this->y = y;
}

size_t x;
float y;
};

int main() {
char buf[8];
Foo* f = (Foo*)buf;
f->set(3.14); // dang
}

但是,如果你把一塊buf,強轉(zhuǎn)成Foo,然后調(diào)用它的getter/setter,則是危險的,有可能破壞前述的對齊要求。

如果你把一個int變量編碼進一個buf,則最好使用memcpy,而不是強轉(zhuǎn)+賦值。

Read-Modify-Write指令

但有時候,我們需要更復(fù)雜的操作指令,而不僅僅是單獨的讀或?qū)懀枰褞讉€動作組合在一起完成某項任務(wù)。

比如語句`++count`對應(yīng)到“讀+修改+寫”三個操作,但這3個操作不是一個原子操作。所以,多線程程序中使用`++count`,多個執(zhí)行流會交錯執(zhí)行,會導(dǎo)致計數(shù)錯誤(通常結(jié)果比預(yù)期數(shù)值?。?/p>

考慮另一個情況:讀+判斷,來我們看一下經(jīng)典單件實現(xiàn):

class Singleton {
static Singleton* instance;
public:
static Singleton* get_instance() {
if (instance == nullptr) {
instance = new Singleton;
}
return instance;
}
};

因為對instance的判斷和`instance = new Singleton`不是原子的,所以,我們需要加鎖:


class Singleton {
static Singleton* instance;
static std::mutex mutex;
public:
static Singleton* get_instance() {
mutex.lock();
if (instance == nullptr)
instance = new Singleton;
mutex.unlock();
return instance;
}
};

但為了性能,更好的方案是加雙檢,代碼變成下面這樣:

static Singleton* get_instance() {
if (instance == nullptr) {
mutex.lock();
if (instance == nullptr) { // 雙檢
instance = new Singleton;
}
mutex.unlock();
return instance;
}
return instance;
}

第一個檢查,如果instance不為空,那么直接返回instance,大多數(shù)時候命中這個情況,因為instance一旦被創(chuàng)建,就不再為空。

如果instance為空,那么再加鎖、然后第二次檢查instance是否為空,為什么要雙檢呢?因為前面的檢查通過后,有可能其他線程創(chuàng)建了實例,導(dǎo)致instance不再為空。

看起來一切都挺好的,高效又縝密。

但雙檢真的安全嗎?這其實是一個非常經(jīng)典的問題。它有2個風險:

  • 首先,編寫者沒有告訴編譯器,必須假設(shè)instance可能被其他線程修改,所以,編譯器可能認為2個if保留一個就可以了,當然,它也可能不做這個優(yōu)化,取決于編譯器的策略,因此,instance必須改為volatile,告訴編譯器,兩次讀都必須從內(nèi)存里加載,避免雙檢被優(yōu)化掉。
  • 就是前面講的原子性,instance指針不能保證一定在8字節(jié)對齊的地方保存,所以,需要用std::atomic代替。

邏輯上,需要幾個操作是一個密不可分的整體,現(xiàn)代CPU通常都直接提供這類原子指令的支持,這類RMW原子指令通常包括:

  • test-and-set(TAS),把1寫入某個內(nèi)存位置并返回舊值;如果原來內(nèi)存位置是1,則返回1,否則原子的寫入1并返回0;只能標識0和1兩種情況
  • fetch_and_add,增加某個內(nèi)存位置的值,并返回舊值;可用來做atomic的后自增
  • compare-and-swap(CAS),比較內(nèi)存位置的值和指定的值,如果相等,則將新值寫入內(nèi)存位置,如果不等,什么也不做;比tas更強

以上所有操作都是在一個內(nèi)存位置執(zhí)行多個動作,但這些操作都是原子單步的,它不會被中斷,也不會穿插進其他操作,這個重要屬性使得RMW指令非常適合用來實現(xiàn)無鎖編程。

雖然CPU在執(zhí)行機器指令的時候,會把它分成更小粒度的微指令(micro-operations),但程序員應(yīng)把關(guān)注點放在微指令上層的原子指令上。

原子操作

前面講的原子指令是硬件層面,不同架構(gòu)甚至不同型號CPU有不同的原子指令,它是CPU層面的東西,跨平臺特性差,用它編寫的代碼不可移植,所以應(yīng)該盡量避免直接使用原子指令。

回到軟件層面,軟件層面的原子操作包括三個層次:

(1) 操作系統(tǒng)層面,linux操作系統(tǒng)提供atomic這種原子類型,配合相關(guān)的編程接口使用,大多數(shù)它只是對原子指令的簡單封裝,但它屏蔽了硬件差異,比原子指令更易用?:

   atomic_read(atomic_t *v)
atomic_set(atomic_t *v, int i)
atomic_inc(atomic_t *v)
atomic_dec(atomic_t *v)
atomic_add(int i, atomic_t *v)
atomic_sub(int i, atomic_t *v)
atomic_inc_and_test(atomic_t *v)
atomic_dec_and_test(atomic_t *v);
atomic_sub_and_test(int i, atomic_t *v)

(2) 編譯器層面,gcc提供原子操作build-in函數(shù),使用gcc編譯c/c++代碼,可以直接使用它們?:

   //其中type對應(yīng)8/16/32/64位整數(shù)
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)
type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)

gcc在實現(xiàn)C++11之后,新的原子接口,以__atomic為前綴,推薦使用下面這些接口:

   type __atomic_add_fetch(type *ptr, type val, int memorder)
type __atomic_sub_fetch(type *ptr, type val, int memorder)
type __atomic_and_fetch(type *ptr, type val, int memorder)
type __atomic_xor_fetch(type *ptr, type val, int memorder)
type __atomic_or_fetch(type *ptr, type val, int memorder)
type __atomic_nand_fetch(type *ptr, type val, int memorder)
type __atomic_fetch_add(type *ptr, type val, int memorder)
type __atomic_fetch_sub(type *ptr, type val, int memorder)
type __atomic_fetch_and(type *ptr, type val, int memorder)
type __atomic_fetch_xor(type *ptr, type val, int memorder)
type __atomic_fetch_or(type *ptr, type val, int memorder)
type __atomic_fetch_nand(type *ptr, type val, int memorder)

(3) 編程語言層面,也通常提供原子操作類型和接口,這也是使用原子操作的推薦方式,它有良好的跨平臺性和可移植性,程序員應(yīng)優(yōu)先使用它們:

  • C11新增了原子操作庫,通過stdatomic.h頭文件提供atomic_fetch_add/atomic_compare_exchange_weak等接口。
  • C++11也新增了原子操作庫,提供一種類型為std::atomic的類模板,它提供++/--/+=/-=/fetch_sub/fetch_add等原子操作接口。
  • 原子操作常用于與順序無關(guān)的場景,比如計數(shù)錯誤的場景,用原子變量改寫后,則會輸出符合預(yù)期的值。
  • 原子操作是編寫Lock-free多線程程序的基礎(chǔ),原子操作只保證原子性,不保證操作順序。
  • 在Lock-free多線程程序中,光有原子操作是不夠的,需要將原子操作和Memory Barrier結(jié)合起來,才能實現(xiàn)免鎖。

文章標題:關(guān)于多線程的一切:原子操作
網(wǎng)址分享:http://www.dlmjj.cn/article/dhddses.html