范縣網(wǎng)站建設(shè)公司精準(zhǔn)營銷通俗來說是什么
指令亂序和線程安全
先來看什么是指令亂序問題以及為什么有指令亂序。程序的代碼執(zhí)行順序有可能被編譯器或CPU根據(jù)某種策略打亂指令執(zhí)行順序,目的是提升程序的執(zhí)行性能,讓程序的執(zhí)行盡可能并行,這就是所謂指令亂序問題。理解指令亂序的策略是很重要的,因為軟件設(shè)計人員可以在正確的位置告訴編譯器或CPU哪里可以允許指令亂序,哪里不能接受指令亂序,從而在保證軟件正確性的同時允許編譯或執(zhí)行層面的性能優(yōu)化。
指令亂序問題需要分為三個層次:
- 第1層是多線程編程中的業(yè)務(wù)邏輯層面的函數(shù)可重入性和線程安全問題;
- 第2層是編譯器編譯優(yōu)化造成的指令亂序;
- 第3層是CPU亂序執(zhí)行指令的問題。
我們在討論CPU指令亂序問題和編譯器指令亂序問題之前,先來簡要討論一下可重入函數(shù)與線程安全相關(guān)的問題。
可重入函數(shù)與線程安全
線程的基本概念
線程(thread)是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它包含在進(jìn)程之中,是進(jìn)程中的實際運(yùn)作單位。一個線程指的是進(jìn)程中一個單一順序的控制流,一個進(jìn)程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務(wù)。一般默認(rèn)一個進(jìn)程中只包含一個線程。
操作系統(tǒng)中的線程概念也被延伸到CPU硬件上,多線程CPU就是在一個CPU上支持同時運(yùn)行多個指令流,而多核CPU就是在一塊芯片上集成了多個CPU核,比如4核8線程CPU芯片就是在集成了4個CPU核,每個CPU核上支持2個線程。
有了多核多線程CPU,操作系統(tǒng)就可以讓不同進(jìn)程運(yùn)行在不同的CPU核的不同線程上,從而大大減少進(jìn)程調(diào)度進(jìn)程切換的資源消耗。傳統(tǒng)上操作系統(tǒng)工作在單核單線程CPU上是通過分時共享CPU來模擬出多個指令執(zhí)行流,從而實現(xiàn)多進(jìn)程和多線程的。
函數(shù)調(diào)用堆??蚣?/h2>
借助函數(shù)調(diào)用堆??梢詫⑽覀儗懙暮瘮?shù)調(diào)用代碼整理成一個順序執(zhí)行的指令流,也就是一個線程,每一個線程都有一個獨(dú)自擁有的函數(shù)調(diào)用堆??臻g,其中函數(shù)參數(shù)和局部變量都存儲在函數(shù)調(diào)用堆??臻g中,因此函數(shù)參數(shù)和局部變量也是線程獨(dú)自擁有的。除了函數(shù)調(diào)用堆??臻g,同一個進(jìn)程的多個線程是共享其他進(jìn)程資源的,比如全局變量是多個線程共享的。
可重入函數(shù)
可重入(reentrant)函數(shù)可以由多于一個任務(wù)并發(fā)使用,而不必?fù)?dān)心數(shù)據(jù)錯誤。相反,不可重入(non-reentrant)函數(shù)不能由超過一個任務(wù)所共享,除非能確保函數(shù)的互斥(或者使用信號量,或者在代碼的關(guān)鍵部分禁用中斷)。可重入函數(shù)可以在任意時刻被中斷,稍后再繼續(xù)運(yùn)行,不會丟失數(shù)據(jù)。可重入函數(shù)要么使用局部變量,要么在使用全局變量時保護(hù)自己的數(shù)據(jù)。
int g = 0;
int function()
{g++; /* switch to another thread */printf("%d", g);
}int function2(int a)
{a++;printf("%d", a);
}
function()函數(shù)為不可重入函數(shù),其中的變量g為全局變量,多個線程同時執(zhí)行function函數(shù)時會出現(xiàn)變量g的值未按照預(yù)想的結(jié)果輸出的情況,function2(int a)為可重入函數(shù),function2函數(shù)中的變量a是對傳入的實參變量的拷貝,并不影響原來傳入的變量。
可重入函數(shù)的基本要求
(1)不為連續(xù)的調(diào)用持有靜態(tài)數(shù)據(jù); ?? ?
(2)不返回指向靜態(tài)數(shù)據(jù)的指針; ?? ?
(3)所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供; ?? ?
(4)使用局部變量,或者通過制作全局?jǐn)?shù)據(jù)的局部變量拷貝來保護(hù)全局?jǐn)?shù)據(jù); ?
(5)使用靜態(tài)數(shù)據(jù)或全局變量時做周密的并行時序分析,通過臨界區(qū)互斥避免臨界區(qū)沖突; ?? ?
(6)絕不調(diào)用任何不可重入函數(shù)。
什么是線程安全?
如果你的代碼所在的進(jìn)程中有多個線程在同時運(yùn)行,而這些線程可能會同時運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。
線程安全問題都是由全局變量及靜態(tài)變量引起的。若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執(zhí)行讀寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
函數(shù)的可重入性與線程安全之間的關(guān)系
可重入的函數(shù)不一定是線程安全的,可能是線程安全的也可能不是線程安全的;同一個可重入的函數(shù)在多個線程中并發(fā)使用時是線程安全的,但不同的可重入函數(shù)(共享全局變量及靜態(tài)變量)在多個線程中并發(fā)使用時會有線程安全問題;不可重入的函數(shù)一定不是線程安全的。
int g = 0;
int plus()
{pthread_mutex_lock(&gplusplus);g++; /* switch to another thread */printf("%d", g);pthread_mutex_unlock(&gplusplus);
}
int minus()
{pthread_mutex_lock(&gminusminus);g--; /* switch to another thread */printf("%d", g);pthread_mutex_unlock(&gminusminus);
}
上述兩個函數(shù)plus() 和 minus() 經(jīng)過加鎖處理之后均稱為可重入函數(shù),但由于使用的是兩個不同的互斥鎖,所以在并發(fā)執(zhí)行時會出現(xiàn)g的值與預(yù)期不一致的現(xiàn)象。故說明可重入函數(shù)不一定是線程安全的。
線程安全和指令亂序
我們這里討論可重入函數(shù)與線程安全本質(zhì)上也是指令亂序執(zhí)行問題,指令亂序問題本質(zhì)上也是線程安全問題,編譯器編譯優(yōu)化或CPU指令亂序執(zhí)行所引發(fā)的程序正確性問題盡管所處的層次不同但本質(zhì)上與此相似,接下來我們分別討論一下CPU指令亂序問題和編譯器指令亂序問題。
CPU的流水線技術(shù)能夠讓指令的執(zhí)行盡可能地并行起來,但是如果兩條指令前后存在依賴關(guān)系,比如數(shù)據(jù)依賴、控制依賴等,此時后一條指令就必需等到前一條指令完成后才能開始執(zhí)行。為了提高流水線的運(yùn)行效率,CPU會對無依賴的前后指令做適當(dāng)?shù)膩y序和調(diào)整,對控制依賴的指令做分支預(yù)測,對內(nèi)存訪問等耗時操作提前預(yù)先處理等,這些都會導(dǎo)致指令亂序執(zhí)行。
編譯器很重要的一項工作就是優(yōu)化我們的代碼以提高性能。這包括在不改變程序正確性的條件下重新排列指令,也就是編譯器指令亂序問題。
CPU指令執(zhí)行的順序一致性
為了提高流水線的運(yùn)行效率,CPU會對無依賴的前后指令做適當(dāng)?shù)膩y序和調(diào)整,對控制依賴的指令做分支預(yù)測,對內(nèi)存訪問等耗時操作提前預(yù)先處理等,這些都會導(dǎo)致指令亂序執(zhí)行。
但是我們編程時一般理解代碼在CPU上的執(zhí)行順序和代碼的邏輯順序是一致的呀?這有點(diǎn)讓人困惑。從單核單線程CPU的角度來看,指令在CPU內(nèi)部可能是亂序執(zhí)行的,但是對外表現(xiàn)卻是順序執(zhí)行的。因為指令集架構(gòu)(ISA)中的指令和寄存器作為CPU的對外接口,CPU只需要把內(nèi)部真實的物理寄存器按照指令的執(zhí)行順序,順序映射到ISA寄存器上,也就是CPU只要將結(jié)果順序地提交到ISA寄存器,就可以保證順序一致性(Sequential consistency)。
多核CPU上指令亂序執(zhí)行
顯然在單核單線程CPU上指令亂序問題被指令集架構(gòu)所屏蔽,但是在多核多線程CPU上依然存在指令亂序執(zhí)行的可能性。比如存在變量x = 0,CPU0上執(zhí)行寫入操作x = 1。接著在CPU1上,執(zhí)行讀取操作依然得到x = 0,這在X86和ARM多核CPU上都是可能出現(xiàn)的。原因是如圖所示CPU核和Cache以及內(nèi)存之間,存在著Store Buffer,當(dāng)x = 1執(zhí)行寫入操作成功后,修改只存在于Store Buffer中,并未寫到cache以及內(nèi)存上,因此CPU1讀取不到最新的x值。除了Store Buffer,而且還可能會有Invalidate Queue,導(dǎo)致CPU1讀不到最新的x值。為了能夠保證多核之間的修改可見性,我們在寫程序的時候需要加上內(nèi)存屏障,例如X86上的mfence指令。
ARM64 CPU指令亂序
對于ARM64架構(gòu)的CPU來說,編程就變得危險多了。除了存在數(shù)據(jù)依賴、控制依賴和地址依賴等不能被亂序執(zhí)行外,其余指令間都有可能存在亂序執(zhí)行。ARM64上沒有依賴關(guān)系的讀后讀、寫后寫、讀后寫和寫后讀都是可以亂序執(zhí)行的。ARM64架構(gòu)下Store Buffer并不是FIFO的,而且還可能存在Invalidate Queue,這讓并發(fā)編程變得困難重重??傊?strong>ARM64是弱內(nèi)存序模型,因為精簡指令集把訪存指令和運(yùn)算指令分開了,為了性能允許幾乎所有的指令亂序,但前提是不影響程序的正確性。因此ARM64架構(gòu)的指令亂序問題需要引入不同類型的barrier來保證程序的正確性。
需要特別指出的是ARM64允許指令亂序執(zhí)行是出于性能的考慮,這是架構(gòu)特性,不是漏洞。但是指令亂序的影響卻給系統(tǒng)可靠性帶來了風(fēng)險,驅(qū)動模塊、基礎(chǔ)軟件和應(yīng)用軟件都要做排查和設(shè)計優(yōu)化。
高級語言定義了邏輯關(guān)系,邏輯關(guān)系與應(yīng)用程序的業(yè)務(wù)邏輯有關(guān);編譯器將內(nèi)含邏輯關(guān)系的高級語言代碼翻譯成機(jī)器語言或匯編語言,其中就定義了數(shù)據(jù)依賴、控制依賴和地址依賴等依賴關(guān)系;ARMv8架構(gòu)定義了內(nèi)存模型以及實現(xiàn)處理這些依賴關(guān)系的機(jī)器語言指令,從而防止有依賴的指令亂序執(zhí)行影響程序正確性。
顯然CPU指令亂序與硬件內(nèi)存模型及防止指令亂序的機(jī)器語言指令內(nèi)部實現(xiàn)緊密相關(guān),這些需要深入到處理器微架構(gòu)深處才能一探究竟,與我們專注于Linux內(nèi)核的目標(biāo)不符,這里不再深入探討它。但是我們需要清楚的一點(diǎn)是,CPU僅能看到機(jī)器指令或匯編指令序列中的數(shù)據(jù)依賴、控制依賴和地址依賴等依賴關(guān)系,并不能理解高級語言中定義的邏輯關(guān)系,因此CPU指令亂序執(zhí)行和編譯優(yōu)化指令亂序都可能會破壞高級語言中定義的邏輯關(guān)系,這是我們學(xué)習(xí)指令亂序問題的原因。
編譯器指令亂序問題
編譯器很重要的一項工作就是優(yōu)化我們的代碼以提高性能。這包括在不改變程序正確性的條件下重新排列指令,也就是編譯器指令亂序問題。
因為編譯器不知道什么樣的代碼需要線程安全,所以編譯器假設(shè)代碼都是單線程執(zhí)行的,也就是編譯器對函數(shù)的可重入問題是沒有感知的,因此編譯器進(jìn)行指令重排優(yōu)化只能保證是單線程安全。因此當(dāng)多線程應(yīng)用程序的邏輯關(guān)系在編譯器重新排序指令的時候可能影響程序正確性時,除非你顯式告訴編譯器,我不需要重排指令順序,否則編譯器可能會在優(yōu)化指令順序時影響程序的正確性。這一部分我們一起探究編譯器編譯優(yōu)化相關(guān)的指令亂序問題。
編譯器屏障
在閱讀Linux內(nèi)核源代碼時,會看到額外插入的匯編指令如下,是告訴編譯器不要優(yōu)化指令順序。如下代碼摘自Linux內(nèi)核源代碼include/linux/compiler-gcc.h。
#define barrier() __asm__ __volatile__("": : :"memory")
如上代碼定義的宏barrier()就是常說的編譯器屏障(compiler barriers),它的主要用途就是告訴編譯器不要優(yōu)化重排指令順序。為了說明這個問題我們用C語言代碼及對應(yīng)的ARM64匯編代碼簡要說明指令亂序造成的問題及編譯器屏障的作用。
編譯器優(yōu)化造成指令亂序問題
編譯器的主要工作就是將高級語言源代碼翻譯成機(jī)器指令,當(dāng)然翻譯的過程中編譯器還會進(jìn)行編譯優(yōu)化以提高代碼的執(zhí)行效率。編譯優(yōu)化主要就是在不影響程序正確性的情況下對機(jī)器指令順序重排從而統(tǒng)籌調(diào)度CPU資源改善程序性能,但是對于多線程應(yīng)用程序編譯器并不能理解程序的并發(fā)執(zhí)行邏輯,很可能會好心干壞事。為了說明編譯優(yōu)化指令亂序造成的問題,我們考慮下面的compiler_reordering.c文件中C語言函數(shù)function的代碼。
int flag, data;int function(void)
{data = data + 1;flag = 1;
}
編譯時未開啟編譯優(yōu)化?
gcc -S compiler_reordering.c -o compiler_reordering.s
function:adrp x0, :got:dataldr x0, [x0, #:got_lo12:data]ldr w0, [x0] // load data to w0add w1, w0, 1 // w1 = w0 + 1adrp x0, :got:dataldr x0, [x0, #:got_lo12:data]str w1, [x0] // data = data + 1adrp x0, :got:flagldr x0, [x0, #:got_lo12:flag]mov w1, 1 // w1 = 1str w1, [x0] // flag = 1nopret
編譯時開啟編譯優(yōu)化??
gcc -O2 -S compiler_reordering.c -o compiler_reordering_O2.s
function:adrp x1, :got:dataadrp x3, :got:flagmov w4, 1 // w4 = 1ldr x1, [x1, #:got_lo12:data]ldr x3, [x3, #:got_lo12:flag]ldr w2, [x1] // load data to w2str w4, [x3] // flag = 1add w2, w2, w4 // w2 = w2 + 1str w2, [x1] // data = data + 1ret
與上述C語言函數(shù)function中的代碼比較,這段優(yōu)化后的ARM64匯編代碼的執(zhí)行順序是不同的。C代碼中是先存儲了data的值,后存儲了flag的值,而優(yōu)化后的ARM64匯編代碼正好相反,先存儲了flag,后保存了data。
這就是編譯器指令亂序問題的典型范例。為什么編譯器會這么做呢?對于單線程來說,data和 flag的寫入順序,編譯器認(rèn)為沒有任何問題的。并且最終的結(jié)果data和flag的值也是正確的。
實際上這種編譯器指令亂序問題在大部分情況下是沒有問題的。但是在某些情況下可能會引入問題。例如我們使用的全局變量flag標(biāo)記共享數(shù)據(jù)data是否就緒。另外一個線程檢測到flag == 1就認(rèn)為data已經(jīng)就緒,而由于編譯器指令亂序,實際上data的值可能還沒有存入內(nèi)存。
下面我們加入內(nèi)存屏障,再來看看編譯后產(chǎn)生的匯編文件。?
#define barrier() __asm__ __volatile__("": : :"memory")int flag, data;int function(void)
{data = data + 1;barrier();flag = 1;
}
function:adrp x0, :got:dataldr x0, [x0, #:got_lo12:data]ldr w1, [x0] // load data to w1add w1, w1, 1 // w1 = w1 + 1str w1, [x0] // data = data + 1adrp x1, :got:flagmov w2, 1 // w2 = 1ldr x1, [x1, #:got_lo12:flag]str w2, [x1] // flag = 1ret
barrier就是編譯器提供的內(nèi)存屏障,作用是告訴編譯器內(nèi)存中的值已經(jīng)改變,之前對內(nèi)存的緩存(緩存到寄存器)都需要拋棄,barrier之后的內(nèi)存操作需要重新從內(nèi)存加載,而不能使用之前寄存器緩存的值。可以防止編譯器優(yōu)化barrier前后的內(nèi)存訪問順序。barrier就像是代碼中的一道不可逾越的屏障,barrier前的內(nèi)存讀寫操作不能跑到barrier后面;同樣barrier后面的內(nèi)存讀寫操作不能在barrier之前。
以上內(nèi)容為中科大軟件學(xué)院《高級軟件工程》課后總結(jié),感謝孟寧老師的傾心教授,老師講的太好啦(^_^)
參考資料:《代碼中的軟件工程》? ? 孟寧? 編著