突然想聊聊這個(gè)話(huà)題,是因?yàn)橹跎系囊粋€(gè)問(wèn)題多次出現(xiàn)在了我的Timeline里:請(qǐng)問(wèn),多個(gè)線(xiàn)程可以讀一個(gè)變量,只有一個(gè)線(xiàn)程可以對(duì)這個(gè)變量進(jìn)行寫(xiě),到底要不要加鎖?可惜的是很多高票答案語(yǔ)焉不詳,甚至有所錯(cuò)漏。所以我想在這篇文章里斗膽聊聊這個(gè)水挺深的問(wèn)題。受限于個(gè)人水平,文章若有錯(cuò)漏,還望讀者不吝賜教。
首先約定,由于CPU的架構(gòu)和設(shè)計(jì)浩如煙海,本文站在工程師的角度,只談IA32/AMD64(x86-64)架構(gòu),不討論其他架構(gòu)的細(xì)節(jié)和差異。并且文章中主要引用Intel的文檔予以佐證,不關(guān)注AMD在實(shí)現(xiàn)細(xì)節(jié)上的差異。
眾所周知,當(dāng)一個(gè)執(zhí)行中的程序的數(shù)據(jù)被多個(gè)執(zhí)行流并發(fā)訪(fǎng)問(wèn)的時(shí)候,就會(huì)涉及到同步(Synchronization)的問(wèn)題。同步的目的是保證不同執(zhí)行流對(duì)共享數(shù)據(jù)并發(fā)操作的一致性。早在單核時(shí)代,使用鎖或者原子變量就很容易達(dá)成這一目的。甚至因?yàn)镃PU的一些訪(fǎng)存特性,對(duì)某些內(nèi)存對(duì)齊數(shù)據(jù)的讀或?qū)懸簿哂性拥奶匦浴?/p>
比如,在《Intel? 64 and IA-32 Architectures Software Developer’s Manual》的第三卷System Programming Guide的Chapter 8 Multiple-Processor Management里,就給出了這樣的說(shuō)明:
也就是說(shuō),有些內(nèi)存對(duì)齊的數(shù)據(jù)的訪(fǎng)問(wèn)在CPU層面就是原子進(jìn)行的(注意這里說(shuō)的只是單次的讀或者寫(xiě),類(lèi)似普通變量i的i++操作不止一次內(nèi)存訪(fǎng)問(wèn))。此時(shí),環(huán)形隊(duì)列(Ring buffer)這種數(shù)據(jù)結(jié)構(gòu)在某些架構(gòu)的單核CPU上,只有一個(gè)Reader和一個(gè)Writer的情況下是不需要額外同步措施的。原因就是read_index和writer_index的寫(xiě)操作在滿(mǎn)足對(duì)齊內(nèi)存訪(fǎng)問(wèn)的情況下是原子的,不需要額外的同步措施。注意這里我加粗了單核CPU這個(gè)關(guān)鍵字,那么到了多核心處理器的今天,該操作就不是原子了嗎?不,依舊是原子的,但是出現(xiàn)了其他的干擾因素迫使可能需要額外的同步措施才能保證原本無(wú)鎖代碼的正確運(yùn)行。
首先是現(xiàn)代編譯器的代碼優(yōu)化和編譯器指令重排可能會(huì)影響到代碼的執(zhí)行順序。編譯期指令重排是通過(guò)調(diào)整代碼中的指令順序,在不改變代碼語(yǔ)義的前提下,對(duì)變量訪(fǎng)問(wèn)進(jìn)行優(yōu)化。從而盡可能的減少對(duì)寄存器的讀取和存儲(chǔ),并充分復(fù)用寄存器。但是編譯器對(duì)數(shù)據(jù)的依賴(lài)關(guān)系判斷只能在單執(zhí)行流內(nèi),無(wú)法判斷其他執(zhí)行流對(duì)競(jìng)爭(zhēng)數(shù)據(jù)的依賴(lài)關(guān)系。就拿無(wú)鎖環(huán)形隊(duì)列來(lái)說(shuō),如果Writer做的是先放置數(shù)據(jù),再更新索引的行為。如果索引先于數(shù)據(jù)更新,Reader就有可能會(huì)因?yàn)榕袛嗨饕迅露x到臟數(shù)據(jù)。
那禁止編譯器對(duì)該類(lèi)變量的優(yōu)化,解決了編譯期的重排序就沒(méi)事了嗎?不,CPU還有亂序執(zhí)行(Out-of-Order Execution)的特性。流水線(xiàn)(Pipeline)和亂序執(zhí)行是現(xiàn)代CPU基本都具有的特性。機(jī)器指令在流水線(xiàn)中經(jīng)歷取指、譯碼、執(zhí)行、訪(fǎng)存、寫(xiě)回等操作。為了CPU的執(zhí)行效率,流水線(xiàn)都是并行處理的,在不影響語(yǔ)義的情況下。處理器次序(Process Ordering,機(jī)器指令在CPU實(shí)際執(zhí)行時(shí)的順序)和程序次序(Program Ordering,程序代碼的邏輯執(zhí)行順序)是允許不一致的,即滿(mǎn)足As-if-Serial特性。顯然,這里的不影響語(yǔ)義依舊只能是保證指令間的顯式因果關(guān)系,無(wú)法保證隱式因果關(guān)系。即無(wú)法保證語(yǔ)義上不相關(guān)但是在程序邏輯上相關(guān)的操作序列按序執(zhí)行。從此單核時(shí)代CPU的Self-Consistent特性在多核時(shí)代已不存在,多核CPU作為一個(gè)整體看,不再滿(mǎn)足Self-Consistent特性。
簡(jiǎn)單總結(jié)一下,如果不做多余的防護(hù)措施,單核時(shí)代的無(wú)鎖環(huán)形隊(duì)列在多核CPU中,一個(gè)CPU核心上的Writer寫(xiě)入數(shù)據(jù),更新index后。另一個(gè)CPU核心上的Reader依靠這個(gè)index來(lái)判斷數(shù)據(jù)是否寫(xiě)入的方式不一定可靠。index有可能先于數(shù)據(jù)被寫(xiě)入,從而導(dǎo)致Reader讀到臟數(shù)據(jù)。
所有的麻煩到這里就結(jié)束了嗎?當(dāng)然不,還有Cache的問(wèn)題。前文提到的都是順序一致性(Sequential Consistency)的問(wèn)題,沒(méi)有涉及Cache一致性(Cache Coherence)的問(wèn)題。雖然說(shuō)一般情況下程序員只需要關(guān)注順序一致性即可,但是區(qū)分清楚這兩個(gè)概念也能更好的解釋內(nèi)存屏障(Memory Barrier)。
開(kāi)始提到Cache一致性協(xié)議之前,先介紹兩個(gè)名詞:
Load/Read CPU讀操作,是指將內(nèi)存數(shù)據(jù)加載到寄存器的過(guò)程
Store/Write CPU寫(xiě)操作,是指將寄存器數(shù)據(jù)寫(xiě)回主存的過(guò)程
現(xiàn)代處理器的緩存一般分為三級(jí),由每一個(gè)核心獨(dú)享的L1、L2 Cache,以及所有的核心共享L3 Cache組成:
由于Cache的容量很小,一般都是充分的利用局部性原理,按行/塊來(lái)和主存進(jìn)行批量數(shù)據(jù)交換,以提升數(shù)據(jù)的訪(fǎng)問(wèn)效率。以前寫(xiě)過(guò)一篇《淺析x86架構(gòu)中cache的組織結(jié)構(gòu)》,這里不再贅述。既然各個(gè)核心之間有獨(dú)立的Cache存儲(chǔ)器,那么這些存儲(chǔ)器之間的數(shù)據(jù)同步就是個(gè)比較復(fù)雜的事情。緩存數(shù)據(jù)的一致性由緩存一致性協(xié)議保證。這里比較經(jīng)典的當(dāng)屬M(fèi)ESI協(xié)議。Intel的處理器使用從MESI中演化出的MESIF協(xié)議,而AMD使用MOESI協(xié)議。緩存一致性協(xié)議的細(xì)節(jié)超出了本文的討論范圍,有興趣的讀者可以自行研究。
傳統(tǒng)的MESI協(xié)議中有兩個(gè)行為的執(zhí)行成本比較大。一個(gè)是將某個(gè)Cache Line標(biāo)記為Invalid狀態(tài),另一個(gè)是當(dāng)某Cache Line當(dāng)前狀態(tài)為Invalid時(shí)寫(xiě)入新的數(shù)據(jù)。所以CPU通過(guò)Store Buffer和Invalidate Queue組件來(lái)降低這類(lèi)操作的延時(shí)。如圖:
當(dāng)一個(gè)核心在Invalid狀態(tài)進(jìn)行寫(xiě)入時(shí),首先會(huì)給其它CPU核發(fā)送Invalid消息,然后把當(dāng)前寫(xiě)入的數(shù)據(jù)寫(xiě)入到Store Buffer中。然后異步在某個(gè)時(shí)刻真正的寫(xiě)入到Cache Line中。當(dāng)前CPU核如果要讀Cache Line中的數(shù)據(jù),需要先掃描Store Buffer之后再讀取Cache Line(Store-Buffer Forwarding)。但是此時(shí)其它CPU核是看不到當(dāng)前核的Store Buffer中的數(shù)據(jù)的,要等到Store Buffer中的數(shù)據(jù)被刷到了Cache Line之后才會(huì)觸發(fā)失效操作。而當(dāng)一個(gè)CPU核收到Invalid消息時(shí),會(huì)把消息寫(xiě)入自身的Invalidate Queue中,隨后異步將其設(shè)為Invalid狀態(tài)。和Store Buffer不同的是,當(dāng)前CPU核心使用Cache時(shí)并不掃描Invalidate Queue部分,所以可能會(huì)有極短時(shí)間的臟讀問(wèn)題。當(dāng)然這里的Store Buffer和Invalidate Queue的說(shuō)法是針對(duì)一般的SMP架構(gòu)來(lái)說(shuō)的,不涉及具體架構(gòu)。事實(shí)上除了Store Buffer和Load Buffer,流水線(xiàn)為了實(shí)現(xiàn)并行處理,還有Line Fill Buffer/Write Combining Buffer 等組件,參考文獻(xiàn)8-10給出了相關(guān)的資料可以進(jìn)一步閱讀。
-
寄存器
+關(guān)注
關(guān)注
31文章
5609瀏覽量
130035 -
cpu
+關(guān)注
關(guān)注
68文章
11285瀏覽量
225144 -
編譯器
+關(guān)注
關(guān)注
1文章
1672瀏覽量
51679
原文標(biāo)題:淺墨: 聊聊原子變量、鎖、內(nèi)存屏障那點(diǎn)事(1)
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
從硬件引申出內(nèi)存屏障,帶你深入了解Linux內(nèi)核RCU
ARM體系結(jié)構(gòu)之內(nèi)存序與內(nèi)存屏障
詳解Linux內(nèi)核鎖的原子操作
MCU上的無(wú)鎖原子讀操作
CPU和內(nèi)存的那點(diǎn)事兒
導(dǎo)致ARM內(nèi)存屏障的原因究竟有哪些
學(xué)習(xí)下ARM內(nèi)存屏障(memory barrier)指令
內(nèi)存屏障是什么
聊聊原子變量、鎖、內(nèi)存屏障那點(diǎn)事(2)
Linux內(nèi)核的內(nèi)存屏障的原理和用法分析
Rust原子類(lèi)型和內(nèi)存排序
一文徹底搞懂內(nèi)存屏障與volatile
小科普|聊聊網(wǎng)線(xiàn)那些事
如何實(shí)現(xiàn)一個(gè)多讀多寫(xiě)的線(xiàn)程安全的無(wú)鎖隊(duì)列
聊聊原子變量、鎖、內(nèi)存屏障那點(diǎn)事(1)
評(píng)論