一文搞懂高效并發(fā)編程:無(wú)鎖編程的秘密與實(shí)戰(zhàn)
無(wú)鎖編程是一種并發(fā)編程的技術(shù),旨在避免使用傳統(tǒng)的鎖機(jī)制來(lái)保護(hù)共享數(shù)據(jù)。相比有鎖編程,無(wú)鎖編程可以提供更高的并發(fā)性能和可伸縮性。在無(wú)鎖編程中,線程或進(jìn)程通過(guò)使用原子操作、CAS(Compare-and-Swap)等技術(shù)來(lái)實(shí)現(xiàn)對(duì)共享數(shù)據(jù)的訪問(wèn)和修改,而不需要依賴(lài)互斥鎖。
無(wú)鎖編程常用于高并發(fā)場(chǎng)景,如網(wǎng)絡(luò)服務(wù)器、多線程應(yīng)用程序等。它可以減少線程之間的競(jìng)爭(zhēng)和阻塞,并且能夠充分利用多核處理器的計(jì)算能力。然而,由于無(wú)鎖編程對(duì)開(kāi)發(fā)者要求較高,在設(shè)計(jì)和實(shí)現(xiàn)上也更加復(fù)雜,需要考慮線程安全性、內(nèi)存模型等因素。
一、無(wú)鎖編程概述
1.1什么是無(wú)鎖編程
無(wú)鎖編程,即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是在沒(méi)有線程被阻塞的情況下實(shí)現(xiàn)變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization),實(shí)現(xiàn)非阻塞同步的方案稱(chēng)為“無(wú)鎖編程算法”。
為什么要非阻塞同步,使用lock實(shí)現(xiàn)線程同步有非常多缺點(diǎn):
- 產(chǎn)生競(jìng)爭(zhēng)時(shí),線程被阻塞等待,無(wú)法做到線程實(shí)時(shí)響應(yīng)
- dead lock
- live lock
- 優(yōu)先級(jí)反轉(zhuǎn)
- 使用不當(dāng),造成性能下降
假設(shè)在不使用 lock 的情況下,實(shí)現(xiàn)變量同步,那就會(huì)避免非常多問(wèn)題。盡管眼下來(lái)看,無(wú)鎖編程并不能替代 lock。
實(shí)現(xiàn)級(jí)別
非同步阻塞的實(shí)現(xiàn)分為三個(gè)級(jí)別:wait-free/lock-free/obstruction-free
(1)wait-free
- 最理想的模式,整個(gè)操作保證每一個(gè)線程在有限步驟下完畢
- 保證系統(tǒng)級(jí)吞吐(system-wide throughput)以及無(wú)線程饑餓
- 截止2011年,沒(méi)有多少詳細(xì)的實(shí)現(xiàn)。即使實(shí)現(xiàn)了,也須要依賴(lài)于詳細(xì)CPU
(2)lock-free
- 同意個(gè)別線程饑餓,但保證系統(tǒng)級(jí)吞吐。
- 確保至少有一個(gè)線程可以繼續(xù)運(yùn)行。
- wait-free的算法必然也是lock-free的。
(3)obstruction-free
在不論什么時(shí)間點(diǎn),一個(gè)線程被隔離為一個(gè)事務(wù)進(jìn)行運(yùn)行(其它線程suspended),而且在有限步驟內(nèi)完畢。在運(yùn)行過(guò)程中,一旦發(fā)現(xiàn)數(shù)據(jù)被改動(dòng)(採(cǎi)用時(shí)間戳、版本),則回滾,也叫做樂(lè)觀鎖,即樂(lè)觀并發(fā)控制(OOC)。
事務(wù)的過(guò)程是:
- 讀取,并寫(xiě)時(shí)間戳
- 準(zhǔn)備寫(xiě)入,版本號(hào)校驗(yàn)
- 檢驗(yàn)通過(guò)則寫(xiě)入,檢驗(yàn)不通過(guò),則回滾
lock-free必然是obstruction-free的。
1.2為什么要無(wú)鎖?
首先是性能考慮。通信項(xiàng)目一般對(duì)性能有極致的追求,這是我們使用無(wú)鎖的重要原因。當(dāng)然,無(wú)鎖算法如果實(shí)現(xiàn)的不好,性能可能還不如使用鎖,所以我們選擇比較擅長(zhǎng)的數(shù)據(jù)結(jié)構(gòu)和算法進(jìn)行l(wèi)ock-free實(shí)現(xiàn),比如Queue,對(duì)于比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)和算法我們通過(guò)lock來(lái)控制,比如Map(雖然我們實(shí)現(xiàn)了無(wú)鎖Hash,但是大小是限定的,而Map是大小不限定的),對(duì)于性能數(shù)據(jù),后續(xù)文章會(huì)給出無(wú)鎖和有鎖的對(duì)比。
次要是避免鎖的使用引起的錯(cuò)誤和問(wèn)題:
- 死鎖(dead lock):兩個(gè)以上線程互相等待
- 鎖護(hù)送(lock convoy):多個(gè)同優(yōu)先級(jí)的線程反復(fù)競(jìng)爭(zhēng)同一個(gè)鎖,搶占鎖失敗后強(qiáng)制上下文切換,引起性能下降
- 優(yōu)先級(jí)反轉(zhuǎn)(priority inversion):低優(yōu)先級(jí)線程擁有鎖時(shí)被中優(yōu)先級(jí)的線程搶占,而高優(yōu)先級(jí)的線程因?yàn)樯暾?qǐng)不到鎖被阻塞。
1.3如何無(wú)鎖?
在現(xiàn)代的 CPU 處理器上,很多操作已經(jīng)被設(shè)計(jì)為原子的,比如對(duì)齊讀(Aligned Read)和對(duì)齊寫(xiě)(Aligned Write)等。Read-Modify-Write(RMW)操作的設(shè)計(jì)讓執(zhí)行更復(fù)雜的事務(wù)操作變成了原子操作,當(dāng)有多個(gè)寫(xiě)入者想對(duì)相同的內(nèi)存進(jìn)行修改時(shí),保證一次只執(zhí)行一個(gè)操作。
RMW 操作在不同的 CPU 家族中是通過(guò)不同的方式來(lái)支持的:
- x86/64 和 Itanium 架構(gòu)通過(guò) Compare-And-Swap (CAS) 方式來(lái)實(shí)現(xiàn)
- PowerPC、MIPS 和 ARM 架構(gòu)通過(guò) Load-Link/Store-Conditional (LL/SC) 方式來(lái)實(shí)現(xiàn)
在x64下進(jìn)行實(shí)踐的,用的是CAS操作,CAS操作是lock-free技術(shù)的基礎(chǔ),我們可以用下面的代碼來(lái)描述:
template <class T>
bool CAS(T* addr, T expected, T value)
{
if (*addr == expected)
{
*addr = value;
return true;
}
return false;
}
在GCC中,CAS操作如下所示:
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
這兩個(gè)函數(shù)提供原子的比較和交換,如果*ptr == oldval,就將newval寫(xiě)入*ptr,第一個(gè)函數(shù)在相等并寫(xiě)入的情況下返回true,第二個(gè)函數(shù)的內(nèi)置行為和第一個(gè)函數(shù)相同,只是它返回操作之前的值。
后面的可擴(kuò)展參數(shù)(...)用來(lái)指出哪些變量需要memory barrier,因?yàn)槟壳癵cc實(shí)現(xiàn)的是full barrier,所以可以略掉這個(gè)參數(shù),除過(guò)CAS操作,GCC還提供了其他一些原子操作,可以在無(wú)鎖算法中靈活使用:
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, ...)
_sync_*系列的built-in函數(shù),用于提供加減和邏輯運(yùn)算的原子操作。這兩組函數(shù)的區(qū)別在于第一組返回更新前的值,第二組返回更新后的值。
無(wú)鎖算法感觸最深的是復(fù)雜度的分解,比如多線程對(duì)于一個(gè)雙向鏈表的插入或刪除操作,如何能一步一步分解成一個(gè)一個(gè)串聯(lián)的原子操作,并能保證事務(wù)內(nèi)存的一致性。
1.4從傳統(tǒng)到無(wú)鎖:編程范式的轉(zhuǎn)變
在并發(fā)編程的世界中,傳統(tǒng)的鎖編程曾是保障數(shù)據(jù)一致性和線程安全的中流砥柱 。就像在一場(chǎng)熱鬧的集市中,每個(gè)攤位是一個(gè)共享資源,而鎖就是攤位的鑰匙,線程則是想要使用攤位的商人。當(dāng)一個(gè)商人拿到鑰匙(獲取鎖)后,就能在攤位上自由交易(訪問(wèn)和修改資源),其他商人只能在一旁等待,直到鑰匙被歸還(鎖被釋放)。
這種方式雖然有效,但在多線程高并發(fā)的場(chǎng)景下,卻逐漸暴露出諸多局限性。
線程阻塞是最直觀的問(wèn)題。當(dāng)一個(gè)線程持有鎖時(shí),其他試圖獲取同一鎖的線程就會(huì)被阻塞,進(jìn)入等待狀態(tài)。這就好比多個(gè)商人都想使用同一個(gè)攤位,但只有一個(gè)人能拿到鑰匙,其他人只能干等著,白白浪費(fèi)時(shí)間。在高并發(fā)系統(tǒng)中,大量線程的阻塞會(huì)導(dǎo)致上下文切換頻繁,極大地消耗系統(tǒng)資源,降低了程序的執(zhí)行效率 。
死鎖隱患更是讓人頭疼。想象一下,兩個(gè)商人 A 和 B,A 拿著攤位 1 的鑰匙,卻想要使用攤位 2;B 拿著攤位 2 的鑰匙,卻想要使用攤位 1。雙方都不愿意放棄自己手中的鑰匙,又都在等待對(duì)方釋放鑰匙,結(jié)果就陷入了僵局,誰(shuí)也無(wú)法繼續(xù)。在編程中,死鎖一旦發(fā)生,程序就會(huì)陷入停滯,無(wú)法正常運(yùn)行,嚴(yán)重影響系統(tǒng)的穩(wěn)定性 。
傳統(tǒng)鎖編程還會(huì)帶來(lái)較大的資源開(kāi)銷(xiāo)。鎖的獲取、釋放以及維護(hù)鎖的狀態(tài)都需要消耗系統(tǒng)資源。隨著線程數(shù)量的增加,這種開(kāi)銷(xiāo)會(huì)愈發(fā)明顯,成為系統(tǒng)性能提升的瓶頸 。
為了解決這些問(wèn)題,無(wú)鎖編程應(yīng)運(yùn)而生,開(kāi)啟了并發(fā)編程的新篇章。它就像是一場(chǎng)創(chuàng)新的集市改革,不再依賴(lài)鑰匙(鎖)來(lái)分配攤位,而是通過(guò)一種更巧妙的方式,讓商人們能夠更高效地使用攤位資源。無(wú)鎖編程摒棄了傳統(tǒng)的鎖機(jī)制,采用原子操作、CAS(Compare-And-Swap)算法等技術(shù),實(shí)現(xiàn)了多線程對(duì)共享資源的無(wú)阻塞訪問(wèn),讓線程在訪問(wèn)共享資源時(shí)無(wú)需等待鎖的釋放,大大提高了并發(fā)性能 。
二、無(wú)鎖編程的底層原理剖析
無(wú)鎖編程具體使用和考慮到的技術(shù)方法包括:原子操作(atomic operations), 內(nèi)存柵欄(memory barriers), 內(nèi)存順序沖突(memory order), 指令序列一致性(sequential consistency)和順ABA現(xiàn)象等等。
在這其中最基礎(chǔ)最重要的是操作的原子性或說(shuō)原子操作。原子操作可以理解為在執(zhí)行完畢之前不會(huì)被任何其它任務(wù)或事件中斷的一系列操作。原子操作是非阻塞編程最核心基本的部分,沒(méi)有原子操作的話,操作會(huì)因?yàn)橹袛喈惓5雀鞣N原因引起數(shù)據(jù)狀態(tài)的不一致從而影響到程序的正確。
2.1原子操作:無(wú)鎖的基石
原子操作是無(wú)鎖編程的基石,它確保了指令執(zhí)行的不可分割性 。就像在建造高樓時(shí),每一塊基石都必須堅(jiān)實(shí)穩(wěn)固,原子操作對(duì)于無(wú)鎖編程也是如此,是構(gòu)建整個(gè)體系的基礎(chǔ)。在現(xiàn)代處理器中,簡(jiǎn)單類(lèi)型的對(duì)齊讀寫(xiě)操作通常是原子的,這意味著這些操作要么完全執(zhí)行,要么完全不執(zhí)行,不會(huì)出現(xiàn)部分執(zhí)行的情況 。比如對(duì)一個(gè)int類(lèi)型變量的賦值操作int a = 10;,在多線程環(huán)境下,這個(gè)賦值操作要么成功完成,將10賦值給a,要么就沒(méi)有執(zhí)行,不會(huì)出現(xiàn)只賦了一半值的情況。
而 Read-Modify-Write 操作則更進(jìn)一步,允許進(jìn)行更復(fù)雜的原子事務(wù)性操作 。以經(jīng)典的i++操作為例,它實(shí)際上包含了讀取i的值、對(duì)值進(jìn)行加 1 修改、再將修改后的值寫(xiě)回這三個(gè)步驟。在傳統(tǒng)的非原子操作中,這三個(gè)步驟可能會(huì)被線程調(diào)度打斷,導(dǎo)致數(shù)據(jù)不一致 。但在無(wú)鎖編程中,通過(guò)原子的 Read-Modify-Write 操作,這三個(gè)步驟被視為一個(gè)整體,不可分割,從而保證了操作的原子性和數(shù)據(jù)的一致性 。
假設(shè)線程 A 和線程 B 都要對(duì)共享變量i執(zhí)行i++操作,如果沒(méi)有原子操作的保證,可能會(huì)出現(xiàn)線程 A 讀取i的值后,還沒(méi)來(lái)得及寫(xiě)回,線程 B 就讀取了相同的值,最終導(dǎo)致i只增加了 1,而不是 2。但有了原子操作,就能確保兩個(gè)線程的i++操作都能正確執(zhí)行,i最終會(huì)增加 2 。
2.2CAS 算法:核心機(jī)制解析
CAS(Compare-And-Swap)算法是無(wú)鎖編程的核心機(jī)制,它如同無(wú)鎖編程的 “大腦”,指揮著各個(gè)線程有序地訪問(wèn)共享資源 。CAS 算法包含三個(gè)參數(shù):V(要更新的變量)、E(預(yù)期值)、N(新值) 。其工作原理是:當(dāng)且僅當(dāng)變量 V 的值等于預(yù)期值 E 時(shí),才會(huì)將變量 V 的值更新為新值 N;如果 V 的值和 E 的值不同,那就說(shuō)明已經(jīng)有其他線程對(duì) V 進(jìn)行了更新,當(dāng)前線程則什么都不做,最后 CAS 返回當(dāng)前 V 的真實(shí)值 。
在一個(gè)多線程的計(jì)數(shù)器場(chǎng)景中,假設(shè)有多個(gè)線程都要對(duì)計(jì)數(shù)器count進(jìn)行加 1 操作 。每個(gè)線程在執(zhí)行加 1 操作時(shí),會(huì)先讀取count的當(dāng)前值作為預(yù)期值 E,然后計(jì)算新值 N(即 E + 1),接著使用 CAS 算法嘗試將count的值更新為 N 。如果此時(shí)count的值仍然等于預(yù)期值 E,說(shuō)明在讀取和嘗試更新的過(guò)程中沒(méi)有其他線程修改過(guò)count,那么更新操作就會(huì)成功;反之,如果count的值已經(jīng)被其他線程修改,不等于預(yù)期值 E,更新操作就會(huì)失敗,線程會(huì)重新讀取count的當(dāng)前值,再次嘗試 。通過(guò)這種不斷比較和交換的方式,CAS 算法實(shí)現(xiàn)了無(wú)鎖同步,避免了傳統(tǒng)鎖機(jī)制帶來(lái)的線程阻塞和性能開(kāi)銷(xiāo) 。
2.3內(nèi)存屏障與順序一致性
在多線程環(huán)境中,由于處理器的優(yōu)化和指令重排序等原因,可能會(huì)導(dǎo)致內(nèi)存操作的順序與程序代碼中編寫(xiě)的順序不一致,從而引發(fā)數(shù)據(jù)一致性問(wèn)題 。內(nèi)存屏障就像是一個(gè) “交通警察”,負(fù)責(zé)維護(hù)內(nèi)存操作的順序,確保不同線程對(duì)內(nèi)存操作的順序可見(jiàn)性 。內(nèi)存屏障通過(guò)阻止處理器對(duì)其前后的內(nèi)存操作進(jìn)行重排序,使得在內(nèi)存屏障之前的操作一定先于屏障之后的操作完成,從而維持了程序的順序一致性 。
在一個(gè)簡(jiǎn)單的多線程賦值和讀取場(chǎng)景中,線程 A 先對(duì)共享變量x進(jìn)行賦值操作x = 10;,然后對(duì)另一個(gè)共享變量y進(jìn)行賦值操作y = 20;;線程 B 則先讀取y的值,再讀取x的值 。如果沒(méi)有內(nèi)存屏障的保證,由于指令重排序,線程 A 的x = 10;和y = 20;操作可能會(huì)被重新排序,導(dǎo)致線程 B 讀取到的y的值是 20,但讀取到的x的值卻還是初始值 0,這就破壞了程序的順序一致性 。但通過(guò)在適當(dāng)?shù)奈恢貌迦雰?nèi)存屏障,就可以保證線程 A 的賦值操作按照代碼順序執(zhí)行,線程 B 讀取到的x和y的值也是符合預(yù)期的,從而確保了程序的正確性 。
三、無(wú)鎖隊(duì)列
無(wú)鎖隊(duì)列是lock-free中最基本的數(shù)據(jù)結(jié)構(gòu),一般應(yīng)用場(chǎng)景是資源分配,比如TimerId的分配,WorkerId的分配,上電內(nèi)存初始?jí)K數(shù)的申請(qǐng)等等。對(duì)于多線程用戶(hù)來(lái)說(shuō),無(wú)鎖隊(duì)列的入隊(duì)和出隊(duì)操作是線程安全的,不用再加鎖控制。
3.1無(wú)鎖隊(duì)列API
ErrCode initQueue(void** queue, U32 unitSize, U32 maxUnitNum);
ErrCode enQueue(void* queue, void* unit);
ErrCode deQueue(void* queue, void* unit);
U32 getQueueSize(void* queue);
BOOL isQueueEmpty(void* queue);
- initQueue初始化隊(duì)列:根據(jù)unitSize和maxUnitNum申請(qǐng)內(nèi)存,并對(duì)內(nèi)存進(jìn)行初始化。
- enQueue入隊(duì):從隊(duì)尾增加元素
- dequeue出隊(duì):從隊(duì)頭刪除元素
- getQueueSize獲取隊(duì)列大?。悍祷仃?duì)列中的元素?cái)?shù)
- isQueueEmpty隊(duì)列是否為空:true表示隊(duì)列為空,false表示隊(duì)列非空
API使用示例
我們以定時(shí)器Id的管理為例,了解一下無(wú)鎖隊(duì)列主要API的使用。
初始化:主線程調(diào)用
ErrCode ret = initQueue(&queue, sizeof(U32), MAX_TMCB_NUM);
if (ret != ERR_TIMER_SUCC)
{
ERR_LOG("lock free init_queue error: %u\n", ret);
return;
}
for (U32 timerId = 0; timerId < MAX_TMCB_NUM; timerId++)
{
ret = enQueue(queue, &timerId);
if (ret != ERR_TIMER_SUCC)
{
ERR_LOG("lock free enqueue error: %u\n", ret);
return;
}
}
timerId分配:多線程調(diào)用
U32 timerId;
ErrCode ret = deQueue(queue, &timerId);
if (ret != ERR_TIMER_SUCC)
{
ERR_LOG("deQueue failed!");
return INVALID_TIMER_ID;
}
timerId回收:多線程調(diào)用
ErrCode ret = enQueue(queue, &timerId);
if (ret != ERR_TIMER_SUCC)
{
ERR_LOG("enQueue failed!");
}
3.2核心實(shí)現(xiàn)
顯然,隊(duì)列操作的核心實(shí)現(xiàn)為入隊(duì)和出隊(duì)操作。
(1)入隊(duì)
入隊(duì)的關(guān)鍵點(diǎn)有下面幾點(diǎn):
- 通過(guò)寫(xiě)次數(shù)確保隊(duì)列元素?cái)?shù)小于最大元素?cái)?shù)
- 獲取next tail的位置
- 將新元素插入到隊(duì)尾
- 尾指針偏移
- 讀次數(shù)加1
①最大元素?cái)?shù)校驗(yàn)
#include <atomic>
template<typename T>
class LockFreeQueue {
private:
struct Node {
T data;
Node* next;
Node(const T& value) : data(value), next(nullptr) {}
};
std::atomic<Node*> head;
std::atomic<Node*> tail;
std::atomic<int> count;
public:
LockFreeQueue() : head(nullptr), tail(nullptr), count(0) {}
void push(const T& value) {
Node* newNode = new Node(value);
newNode->next = nullptr;
Node* prevTail = tail.exchange(newNode, std::memory_order_acq_rel);
if (prevTail != nullptr)
prevTail->next = newNode;
count.fetch_add(1, std::memory_order_release);
}
bool pop(T& value) {
if (head == nullptr)
return false;
Node* oldHead = head.load(std::memory_order_acquire);
Node* newHead = oldHead->next;
if (newHead == nullptr)
return false;
value = newHead->data;
head.store(newHead, std::memory_order_release);
delete oldHead;
count.fetch_sub(1, std::memory_order_release);
return true;
}
int size() const {
return count.load(std::memory_order_relaxed);
}
};
在入隊(duì)操作開(kāi)始,就判斷寫(xiě)次數(shù)是否超過(guò)隊(duì)列元素的最大值,如果已超過(guò),則反饋隊(duì)列已滿(mǎn)的錯(cuò)誤碼,否則通過(guò)CAS操作將寫(xiě)次數(shù)加1。如果CAS操作失敗,說(shuō)明有多個(gè)線程同時(shí)判斷了寫(xiě)次數(shù)都小于隊(duì)列最大元素?cái)?shù),那么只有一個(gè)線程CAS操作成功,其他線程則需要重新做循環(huán)操作。
②獲取next tail的位置
do
{
do
{
nextTail = queueHead->nextTail;
} while (!__sync_bool_compare_and_swap(&queueHead->nextTail, nextTail, (nextTail + 1) % (queueHead->maxUnitNum + 1)));
unitHead = UNIT_HEAD(queue, nextTail);
} while (unitHead->hasUsed);
當(dāng)前next tail的位置就是即將入隊(duì)的元素的目標(biāo)位置,并通過(guò)CAS操作更新隊(duì)列頭中nextTail的值。如果CAS操作失敗,則說(shuō)明其他線程也正在進(jìn)行入隊(duì)操作,并比本線程快,則需要進(jìn)行重新嘗試,從而更新next tail的值,確保該入隊(duì)元素的位置正確。
然而事情并沒(méi)有這么簡(jiǎn)單,由于多線程的搶占,導(dǎo)致隊(duì)列并不是按下標(biāo)大小依次鏈接起來(lái)的,所以要判斷一下next tail的位置是否正被占用。如果next tail的位置正被占用,則需要重新競(jìng)爭(zhēng)next tail,直到next tail的位置是空閑的。
③將新元素插入到隊(duì)尾
初始化新元素:
unitHead->next = LIST_END;
memcpy(UNIT_DATA(queue, nextTail), unit, queueHead->unitSize);
插入到隊(duì)尾:
do
{
listTail = queueHead->listTail;
oldListTail = listTail;
unitHead = UNIT_HEAD(queue, listTail);
if ((++tryTimes) >= 3)
{
while (unitHead->next != LIST_END)
{
listTail = unitHead->next;
unitHead = UNIT_HEAD(queue, listTail);
}
}
} while (!__sync_bool_compare_and_swap(&unitHead->next, LIST_END, nextTail));
通過(guò)CAS操作判斷當(dāng)前指針是否到達(dá)隊(duì)尾,如果到達(dá)隊(duì)尾,則將新元素連接到隊(duì)尾元素之后(next),否則進(jìn)行追趕。
在這里,我們做了優(yōu)化,當(dāng)CAS操作連續(xù)失敗3次后,那么就直接通過(guò)next的遞推找到隊(duì)尾,這樣比CAS操作的效率高很多。我們?cè)跍y(cè)試多線程的隊(duì)列操作時(shí),出現(xiàn)過(guò)一個(gè)線程插入到tail為400多的時(shí)候,已有線程插入到tail為1000多的場(chǎng)景。
④尾指針偏移
do
{
__sync_val_compare_and_swap(&queueHead->listTail, oldListTail, nextTail);
oldListTail = queueHead->listTail;
unitHead = UNIT_HEAD(queue, oldListTail);
nextTail = unitHead->next;
} while (nextTail != LIST_END);
在多線程場(chǎng)景下,隊(duì)尾指針是動(dòng)態(tài)變化的,當(dāng)前尾可能不是新尾了,所以通過(guò)CAS操作更新隊(duì)尾。當(dāng)CAS操作失敗時(shí),說(shuō)明隊(duì)尾已經(jīng)被其他線程更新,此時(shí)不能將nextTail賦值給隊(duì)尾。
⑤讀次數(shù)加1
__sync_fetch_and_add(&queueHead->readCount, 1);
寫(xiě)次數(shù)加1是為了保證隊(duì)列元素的數(shù)不能超過(guò)最大元素?cái)?shù),而讀次數(shù)加1是為了確保不能從空隊(duì)列出隊(duì)。
(2)出隊(duì)
出隊(duì)的關(guān)鍵點(diǎn)有下面幾點(diǎn):
- 通過(guò)讀次數(shù)確保不能從空隊(duì)列出隊(duì)
- 頭指針偏移
- dummy頭指針
- 寫(xiě)次數(shù)減1
①空隊(duì)列校驗(yàn)
do
{
readCount = queueHead->readCount;
if (readCount == 0) return ERR_QUEUE_HAS_EMPTY;
} while (!__sync_bool_compare_and_swap(&queueHead->readCount, readCount, readCount - 1));
讀次數(shù)為0,說(shuō)明隊(duì)列為空,否則通過(guò)CAS操作將讀次數(shù)減1。如果CAS操作失敗,說(shuō)明其他線程已更新讀次數(shù)成功,必須重試,直到成功。
②頭指針偏移
U32 readCount;
do
{
listHead = queueHead->listHead;
unitHead = UNIT_HEAD(queue, listHead);
} while (!__sync_bool_compare_and_swap(&queueHead->listHead, listHead, unitHead->next));
如果CAS操作失敗,說(shuō)明隊(duì)頭指針已經(jīng)在其他線程的操作下進(jìn)行了偏移,所以要重試,直到成功。
③dummy頭指針
我們可以看出,頭元素為head->next,這就是說(shuō)隊(duì)列的第一個(gè)元素都是基于head->next而不是head,這樣設(shè)計(jì)是有原因的。
考慮一個(gè)邊界條件:在隊(duì)列只有一個(gè)元素條件下,如果head和tail指針指向同一個(gè)結(jié)點(diǎn),這樣入隊(duì)操作和出隊(duì)操作本身就需要互斥了。通過(guò)引入一個(gè)dummy頭指針來(lái)解決這個(gè)問(wèn)題,如下圖所示:
圖片
④寫(xiě)次數(shù)減1
__sync_fetch_and_sub(&queueHead->writeCount, 1);
出隊(duì)操作結(jié)束前,要將寫(xiě)次數(shù)減1,以便入隊(duì)操作能成功。
四、無(wú)鎖編程技術(shù)
事實(shí)證明,當(dāng)你試圖滿(mǎn)足無(wú)鎖編程的無(wú)阻塞條件時(shí),會(huì)出現(xiàn)一系列技術(shù):原子操作、內(nèi)存屏障、避免ABA問(wèn)題,等等。從這里開(kāi)始,事情很快變得棘手了。
4.1原子的 Read-Modify-Write 操作
所謂原子操作是指,通過(guò)一種看起來(lái)不可分割的方式來(lái)操作內(nèi)存:線程無(wú)法看到原子操作的中間過(guò)程。在現(xiàn)代的處理器上,有很多操作本身就是的原子的。例如,對(duì)簡(jiǎn)單類(lèi)型的對(duì)齊的讀和寫(xiě)通常就是原子的。
Read-Modify-Write(RMW)操作更進(jìn)一步,它允許你按照原子的方式,操作更復(fù)雜的事務(wù)。當(dāng)一個(gè)無(wú)鎖的算法必須支持多個(gè)寫(xiě)入者時(shí),原子操作會(huì)尤其有用,因?yàn)槎鄠€(gè)線程試圖在同一個(gè)地址上進(jìn)行RMW時(shí),它們會(huì)按“一次一個(gè)”的方式排隊(duì)執(zhí)行這些操作。我已經(jīng)在我的博客中涉及了RMW操作了,如實(shí)現(xiàn) 輕量級(jí)互斥量、遞歸互斥量 和 輕量級(jí)日志系統(tǒng)。
RMW 操作的例子包括:Win32上 的 _InterlockedIncrement,iOS 上的 OSAtomicAdd32 以及 C++11 中的 std::atomic<int>::fetch_add。需要注意的是,C++11 的原子標(biāo)準(zhǔn)不保證其在每個(gè)平臺(tái)上的實(shí)現(xiàn)都是無(wú)鎖的,因此最好要清楚你的平臺(tái)和工具鏈的能力。你可以調(diào)用 std::atomic<>::is_lock_free 來(lái)確認(rèn)一下。
不同的 CPU 系列支持 RMW 的方式也是不同的。例如,PowerPC 和 ARM 提供 load-link/store-conditional 指令,這實(shí)際上是允許你實(shí)現(xiàn)你自定義的底層 RMW 操作。常用的 RMW 操作就已經(jīng)足夠了。
如上面流程圖所述,即使在單處理器系統(tǒng)上,原子的 RMW 操作也是無(wú)鎖編程的必要部分。沒(méi)有原子性的話,一個(gè)線程的事務(wù)會(huì)被中途打斷,這可能會(huì)導(dǎo)致一個(gè)錯(cuò)誤的狀態(tài)。
4.2Compare-And-Swap 循環(huán)
或許,最常討論的 RMW 操作是 compare-and-swap (CAS)。在Win32上,CAS 是通過(guò)如 _InterlockedCompareExchange 等一系列指令來(lái)提供的。通常,程序員會(huì)在一個(gè)事務(wù)中使用 CAS 循環(huán)。這個(gè)模式通常包括:復(fù)制一個(gè)共享的變量至本地變量,做一些特定的工作(改動(dòng)),再試圖使用 CAS 發(fā)布這些改動(dòng)。
void LockFreeQueue::push(Node* newHead)
{
for (;;)
{
// Copy a shared variable (m_Head) to a local.
Node* oldHead = m_Head;
// Do some speculative work, not yet visible to other threads.
newHead->next = oldHead;
// Next, attempt to publish our changes to the shared variable.
// If the shared variable hasn't changed, the CAS succeeds and we return.
// Otherwise, repeat.
if (_InterlockedCompareExchange(&m_Head, newHead, oldHead) == oldHead)
return;
}
}
這樣的循環(huán)仍然有資格作為無(wú)鎖的,因?yàn)槿绻粋€(gè)線程檢測(cè)失敗,意味著有其它線程成功—盡管某些架構(gòu)提供一個(gè) 較弱的CAS變種 。無(wú)論何時(shí)實(shí)現(xiàn)一個(gè)CAS循環(huán),都必須十分小心地避免 ABA 問(wèn)題。
4.3順序一致性
順序一致性(Sequential consistency)意味著,所有線程就內(nèi)存操作的順序達(dá)成一致。這個(gè)順序是和操作在程序源代碼中的順序是一致的。
一種實(shí)現(xiàn)順序一致性的簡(jiǎn)單(但顯然不切實(shí)際)的方法是禁用編譯器優(yōu)化,并強(qiáng)制所有線程在單個(gè)處理器上運(yùn)行。即使線程在任意時(shí)間被搶占和調(diào)度,處理器也永遠(yuǎn)不會(huì)看到自己的內(nèi)存影響。
某些編程語(yǔ)言甚至可以為在多處理器環(huán)境中運(yùn)行的優(yōu)化代碼提供順序一致性。在C ++ 11中,可以將所有共享變量聲明為具有默認(rèn)內(nèi)存排序約束的C ++ 11原子類(lèi)型。
std::atomic X(0), Y(0);
int r1, r2;
void thread1()
{
X.store(1);
r1 = Y.load();
}
void thread2()
{
Y.store(1);
r2 = X.load();
}
因?yàn)?C ++ 11 原子類(lèi)型保證順序一致性,所以結(jié)果 r1 = r2 = 0 是不可能的。為此,編譯器會(huì)在后臺(tái)輸出其他指令——通常是 內(nèi)存圍欄 和/或 RMW 操作。與程序員直接處理內(nèi)存排序的指令相比,那些額外的指令可能會(huì)使實(shí)現(xiàn)效率降低。
4.4內(nèi)存保序
正如前面流程圖所建議的那樣,任何時(shí)候做多核(或者任何對(duì)稱(chēng)多處理器)的無(wú)鎖編程,如果你的環(huán)境不能保證順序一致性,你都必須考慮如何來(lái)防止 內(nèi)存重新排序。
在當(dāng)今的架構(gòu)中,增強(qiáng)內(nèi)存保序性的工具通常分為三類(lèi),它們既防止 編譯器重新排序 又防止 處理器重新排序:
- 一個(gè)輕型的同步或屏障指令
- 一個(gè)完全的內(nèi)存屏障指令
- 提供獲取或釋放語(yǔ)義的內(nèi)存操作
獲取語(yǔ)義可防止按照程序順序?qū)ζ溥M(jìn)行操作的內(nèi)存重新排序,而釋放語(yǔ)義則可防止對(duì)其進(jìn)行操作前的內(nèi)存重新排序。這些語(yǔ)義尤其適用于存在生產(chǎn)者/消費(fèi)者關(guān)系的情況,其中一個(gè)線程發(fā)布一些信息,而另一個(gè)線程讀取它。
不同的處理器有不同的內(nèi)存模型
在內(nèi)存重新排序方面,不同的CPU系列具有不同的習(xí)慣。每個(gè)CPU供應(yīng)商都記錄了這些規(guī)則,并嚴(yán)格遵循了硬件。例如,PowerPC 和 ARM 處理器可以相對(duì)于指令本身更改內(nèi)存存儲(chǔ)的順序,但是通常,英特爾和 AMD 的 x86 / 64 系列處理器不會(huì)。
有人傾向于將這些特定于平臺(tái)的細(xì)節(jié)抽象出來(lái),尤其是C ++ 11為我們提供了編寫(xiě)可移植的無(wú)鎖代碼的標(biāo)準(zhǔn)方法。但是目前,我認(rèn)為大多數(shù)無(wú)鎖程序員至少對(duì)平臺(tái)差異有所了解。如果需要記住一個(gè)關(guān)鍵的區(qū)別,那就是在x86 / 64指令級(jí)別上,每次從內(nèi)存中加載都會(huì)獲取語(yǔ)義,并且每次存儲(chǔ)到內(nèi)存都將提供釋放語(yǔ)義–至少對(duì)于非SSE指令和非寫(xiě)組合內(nèi)存 。因此,過(guò)去通常會(huì)編寫(xiě)可在x86 / 64上運(yùn)行但 在其他處理器上無(wú)法運(yùn)行 的無(wú)鎖代碼。
如果你對(duì)硬件如何處理內(nèi)存重新排序的細(xì)節(jié)感興趣,我建議看看《Is Pararllel Programming Hard》的附錄C。無(wú)論如何,請(qǐng)記住,由于編譯器對(duì)指令的重新排序,也會(huì)發(fā)生內(nèi)存重新排序。
五、無(wú)鎖編程的優(yōu)勢(shì)與應(yīng)用場(chǎng)景
5.1性能飛躍:高并發(fā)下的卓越表現(xiàn)
在高并發(fā)場(chǎng)景中,無(wú)鎖編程的性能優(yōu)勢(shì)格外顯著。以一個(gè)簡(jiǎn)單的計(jì)數(shù)器為例,在傳統(tǒng)鎖編程中,每次對(duì)計(jì)數(shù)器的更新都需要獲取鎖,這會(huì)導(dǎo)致大量線程等待,造成上下文切換頻繁 。假設(shè)一個(gè)應(yīng)用中有 100 個(gè)線程同時(shí)對(duì)計(jì)數(shù)器進(jìn)行 10000 次加 1 操作,使用傳統(tǒng)的synchronized關(guān)鍵字實(shí)現(xiàn)的鎖編程,在高并發(fā)情況下,線程的阻塞和上下文切換會(huì)使得執(zhí)行時(shí)間明顯增加 。而采用無(wú)鎖編程,利用AtomicInteger類(lèi)提供的原子操作,線程無(wú)需等待鎖的釋放,能夠無(wú)阻塞地執(zhí)行加 1 操作 。
通過(guò)實(shí)際測(cè)試,在同樣的硬件環(huán)境和線程數(shù)量下,無(wú)鎖編程實(shí)現(xiàn)的計(jì)數(shù)器操作,其執(zhí)行時(shí)間相較于傳統(tǒng)鎖編程大幅縮短,系統(tǒng)吞吐量顯著提升 。這是因?yàn)闊o(wú)鎖編程減少了鎖競(jìng)爭(zhēng)帶來(lái)的開(kāi)銷(xiāo),讓 CPU 能夠更專(zhuān)注于實(shí)際的業(yè)務(wù)計(jì)算,大大提高了 CPU 的利用率 。在一些對(duì)吞吐量要求極高的互聯(lián)網(wǎng)應(yīng)用中,如電商平臺(tái)的訂單計(jì)數(shù)、社交平臺(tái)的點(diǎn)贊計(jì)數(shù)等,無(wú)鎖編程能夠輕松應(yīng)對(duì)高并發(fā)的讀寫(xiě)操作,保證系統(tǒng)的高效運(yùn)行 。
5.2避免死鎖:穩(wěn)定性的保障
死鎖是傳統(tǒng)鎖編程中一個(gè)令人頭疼的問(wèn)題,它會(huì)導(dǎo)致程序陷入無(wú)限期的等待,無(wú)法繼續(xù)執(zhí)行 。在一個(gè)多線程的文件讀寫(xiě)系統(tǒng)中,線程 A 持有文件讀取鎖,試圖獲取文件寫(xiě)入鎖;而線程 B 持有文件寫(xiě)入鎖,試圖獲取文件讀取鎖,雙方都不釋放自己持有的鎖,就會(huì)陷入死鎖狀態(tài) 。而無(wú)鎖編程從根本上避免了死鎖的產(chǎn)生,因?yàn)樗灰蕾?lài)鎖機(jī)制來(lái)實(shí)現(xiàn)線程同步 。
無(wú)鎖編程使用原子操作和 CAS 算法,線程在訪問(wèn)共享資源時(shí),無(wú)需等待鎖的獲取,而是通過(guò)不斷嘗試原子操作來(lái)更新資源 。這種方式使得線程之間不會(huì)因?yàn)榈却i而相互阻塞,從而杜絕了死鎖的發(fā)生 。在復(fù)雜的分布式系統(tǒng)中,無(wú)鎖編程的這一優(yōu)勢(shì)尤為重要,它能夠確保系統(tǒng)在高并發(fā)和復(fù)雜業(yè)務(wù)邏輯下的穩(wěn)定性,減少因死鎖導(dǎo)致的系統(tǒng)故障和數(shù)據(jù)不一致問(wèn)題 。
5.3應(yīng)用領(lǐng)域大掃描
無(wú)鎖編程在眾多領(lǐng)域都有著廣泛的應(yīng)用,其獨(dú)特的優(yōu)勢(shì)使其成為解決高并發(fā)和性能問(wèn)題的有力工具 。
在高性能計(jì)算領(lǐng)域,無(wú)鎖編程能夠充分發(fā)揮多核處理器的并行計(jì)算能力,提高計(jì)算效率 。在科學(xué)計(jì)算中,如氣象模擬、分子動(dòng)力學(xué)模擬等,需要處理大量的數(shù)據(jù)和復(fù)雜的計(jì)算任務(wù),無(wú)鎖編程可以減少線程之間的同步開(kāi)銷(xiāo),讓各個(gè)核心能夠高效地協(xié)同工作,加速計(jì)算過(guò)程 。
數(shù)據(jù)庫(kù)系統(tǒng)也常常運(yùn)用無(wú)鎖編程來(lái)提升并發(fā)性能 。在多線程并發(fā)訪問(wèn)數(shù)據(jù)庫(kù)時(shí),傳統(tǒng)的鎖機(jī)制會(huì)導(dǎo)致大量線程阻塞,降低數(shù)據(jù)庫(kù)的讀寫(xiě)性能 。而無(wú)鎖編程通過(guò)原子操作和無(wú)鎖數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)了對(duì)數(shù)據(jù)庫(kù)資源的高效訪問(wèn),提高了數(shù)據(jù)庫(kù)的并發(fā)處理能力,能夠更好地滿(mǎn)足高并發(fā)業(yè)務(wù)的需求 。
緩存系統(tǒng)同樣依賴(lài)無(wú)鎖編程來(lái)實(shí)現(xiàn)快速的數(shù)據(jù)讀寫(xiě) 。以Redis為例,它在處理大量并發(fā)請(qǐng)求時(shí),采用了無(wú)鎖的數(shù)據(jù)結(jié)構(gòu)和原子操作,確保了數(shù)據(jù)的一致性和高效讀寫(xiě),使得緩存系統(tǒng)能夠快速響應(yīng)請(qǐng)求,減輕后端數(shù)據(jù)庫(kù)的壓力 。
在消息傳遞和事件驅(qū)動(dòng)系統(tǒng)中,無(wú)鎖編程能夠避免線程阻塞,提高系統(tǒng)的響應(yīng)速度和可伸縮性 。在分布式消息隊(duì)列中,多個(gè)生產(chǎn)者和消費(fèi)者并發(fā)地進(jìn)行消息的發(fā)送和接收,無(wú)鎖編程保證了消息的高效傳遞和處理,使得系統(tǒng)能夠快速響應(yīng)各種事件,滿(mǎn)足實(shí)時(shí)性要求較高的應(yīng)用場(chǎng)景 。
六、無(wú)鎖編程實(shí)踐困境
6.1編程復(fù)雜度提升
無(wú)鎖編程雖然帶來(lái)了性能上的優(yōu)勢(shì),但也極大地提升了編程的復(fù)雜度,對(duì)開(kāi)發(fā)者提出了更高的要求 。在無(wú)鎖編程中,開(kāi)發(fā)者需要深入理解底層硬件和操作系統(tǒng)的并發(fā)機(jī)制,這是因?yàn)闊o(wú)鎖編程依賴(lài)于原子操作、內(nèi)存屏障等底層技術(shù)來(lái)實(shí)現(xiàn)線程安全,而這些技術(shù)與硬件的特性密切相關(guān) 。不同的處理器架構(gòu)可能對(duì)原子操作的支持和實(shí)現(xiàn)方式有所不同,開(kāi)發(fā)者需要清楚地了解這些差異,才能正確地編寫(xiě)無(wú)鎖代碼 。
無(wú)鎖編程的代碼邏輯往往更加復(fù)雜。以實(shí)現(xiàn)一個(gè)無(wú)鎖隊(duì)列為例,在傳統(tǒng)的鎖編程中,使用鎖來(lái)保證隊(duì)列操作的線程安全,代碼邏輯相對(duì)簡(jiǎn)單,只需要在關(guān)鍵操作前后加鎖和解鎖即可 。但在無(wú)鎖編程中,需要使用 CAS 算法等技術(shù)來(lái)實(shí)現(xiàn)無(wú)鎖的入隊(duì)和出隊(duì)操作,這涉及到復(fù)雜的指針操作和狀態(tài)判斷 。每次入隊(duì)操作都需要通過(guò) CAS 算法嘗試將新節(jié)點(diǎn)插入到隊(duì)列尾部,如果插入失敗,還需要根據(jù)失敗的原因進(jìn)行相應(yīng)的處理,可能需要重新獲取隊(duì)列的狀態(tài)并再次嘗試插入 。這種復(fù)雜的邏輯使得代碼的編寫(xiě)和維護(hù)難度大幅增加,容易出現(xiàn)邏輯錯(cuò)誤,對(duì)開(kāi)發(fā)者的編程能力和經(jīng)驗(yàn)是一個(gè)巨大的挑戰(zhàn) 。
6.2ABA 問(wèn)題及解決方案
ABA 問(wèn)題是無(wú)鎖編程中一個(gè)較為棘手的問(wèn)題,它可能導(dǎo)致程序出現(xiàn)意想不到的錯(cuò)誤 。假設(shè)一個(gè)共享變量的初始值為 A,線程 1 讀取到這個(gè)值 A 后,由于某些原因被阻塞 。在這期間,線程 2 將變量的值從 A 改為 B,然后又改回 A 。當(dāng)線程 1 恢復(fù)執(zhí)行時(shí),它通過(guò) CAS 操作檢查變量的值,發(fā)現(xiàn)仍然是 A,就會(huì)認(rèn)為變量沒(méi)有被修改過(guò),從而繼續(xù)執(zhí)行后續(xù)的操作 。但實(shí)際上,變量已經(jīng)經(jīng)歷了兩次變化,這可能會(huì)導(dǎo)致程序的邏輯出現(xiàn)錯(cuò)誤 。
在一個(gè)無(wú)鎖的鏈表結(jié)構(gòu)中,當(dāng)進(jìn)行節(jié)點(diǎn)刪除操作時(shí)就可能出現(xiàn) ABA 問(wèn)題 。假設(shè)鏈表的初始狀態(tài)是 A -> B -> C,線程 1 想要?jiǎng)h除節(jié)點(diǎn) A,它讀取到節(jié)點(diǎn) A 的指針,準(zhǔn)備進(jìn)行刪除操作 。但此時(shí)線程 2 介入,先刪除了節(jié)點(diǎn) A,然后又重新插入了一個(gè)值為 A 的新節(jié)點(diǎn),鏈表變?yōu)?A -> B -> C(這里的新 A 節(jié)點(diǎn)和原來(lái)的 A 節(jié)點(diǎn)不是同一個(gè)對(duì)象) 。線程 1 繼續(xù)執(zhí)行刪除操作時(shí),由于 CAS 操作檢查到節(jié)點(diǎn) A 的指針沒(méi)有變化,就會(huì)誤以為刪除操作可以正常進(jìn)行,但實(shí)際上它刪除的是新插入的節(jié)點(diǎn) A,這就破壞了鏈表的結(jié)構(gòu),導(dǎo)致數(shù)據(jù)不一致 。
為了解決 ABA 問(wèn)題,通常采用帶有版本號(hào)的 CAS 操作 。以 Java 中的AtomicStampedReference類(lèi)為例,它在進(jìn)行 CAS 操作時(shí),不僅會(huì)比較引用值,還會(huì)比較一個(gè)時(shí)間戳(版本號(hào)) 。每次變量發(fā)生變化時(shí),時(shí)間戳也會(huì)相應(yīng)地增加 。在上述鏈表刪除節(jié)點(diǎn)的例子中,使用AtomicStampedReference來(lái)管理鏈表節(jié)點(diǎn)的引用,當(dāng)線程 1 讀取節(jié)點(diǎn) A 的引用和時(shí)間戳后,線程 2 進(jìn)行刪除和重新插入操作時(shí),時(shí)間戳?xí)黾?。線程 1 執(zhí)行刪除操作時(shí),通過(guò) CAS 操作比較引用和時(shí)間戳,發(fā)現(xiàn)時(shí)間戳已經(jīng)改變,就知道變量已經(jīng)被修改過(guò),從而避免了錯(cuò)誤的刪除操作 。除了版本號(hào),還可以使用原子標(biāo)記引用(如AtomicMarkableReference),它通過(guò)一個(gè)標(biāo)記位來(lái)記錄變量是否被修改過(guò),也能有效地解決 ABA 問(wèn)題 。
ABA問(wèn)題可以俗稱(chēng)為“調(diào)包問(wèn)題”,我們先看一個(gè)生活化的例子:
你拿著一個(gè)裝滿(mǎn)錢(qián)的手提箱在飛機(jī)場(chǎng),此時(shí)過(guò)來(lái)了一個(gè)火辣性感的美女,然后她很暖昧地挑逗著你,并趁你不注意的時(shí)候,把用一個(gè)一模一樣的手提箱和你那裝滿(mǎn)錢(qián)的箱子調(diào)了個(gè)包,然后就離開(kāi)了,你看到你的手提箱還在那,于是就提著手提箱去趕飛機(jī)了。
我們?cè)倏匆粋€(gè)CAS化的例子:
若線程對(duì)同一內(nèi)存地址進(jìn)行了兩次讀操作,而兩次讀操作得到了相同的值,通過(guò) "值相同" 來(lái)判定 "值沒(méi)變"是不可靠的。因?yàn)樵谶@兩次讀操作的時(shí)間間隔之內(nèi),另外的線程可能已經(jīng)多次修改了該值,這樣就相當(dāng)于欺騙了前面的線程,使其認(rèn)為 "值沒(méi)變",實(shí)際上值已經(jīng)被篡改過(guò)了。
下面是 ABA 問(wèn)題發(fā)生的過(guò)程:
- T1 線程從共享的內(nèi)存地址讀取值 A;
- T1 線程被搶占,線程 T2 開(kāi)始運(yùn)行;
- T2 線程將共享的內(nèi)存地址中的值由 A 改成 B,然后又改成 A;
- T1 線程繼續(xù)執(zhí)行,讀取共享的內(nèi)存地址中的值 A,認(rèn)為沒(méi)有改變,然后繼續(xù)執(zhí)行
由于 T1 并不知道兩次讀取的值 A 已經(jīng)被 "隱性" 的修改過(guò),所以可能產(chǎn)生無(wú)法預(yù)期的結(jié)果;當(dāng) CAS操作循環(huán)執(zhí)行時(shí),存在多個(gè)線程交錯(cuò)地對(duì)共享的內(nèi)存地址進(jìn)行處理,如果實(shí)現(xiàn)的不正確,將有可能遇到 ABA 問(wèn)題。
6.3調(diào)試?yán)щy與策略
無(wú)鎖編程的調(diào)試難度遠(yuǎn)遠(yuǎn)大于傳統(tǒng)的鎖編程,這是無(wú)鎖編程在實(shí)踐中面臨的又一挑戰(zhàn) 。由于無(wú)鎖編程依賴(lài)于多線程并發(fā)執(zhí)行,線程之間的執(zhí)行順序和時(shí)間片分配是不確定的,這使得問(wèn)題難以復(fù)現(xiàn) 。一個(gè)在測(cè)試環(huán)境中偶爾出現(xiàn)的問(wèn)題,可能在再次運(yùn)行時(shí)就不會(huì)出現(xiàn),這給定位和解決問(wèn)題帶來(lái)了極大的困難 。
無(wú)鎖編程中線程執(zhí)行順序的不確定性也增加了跟蹤和調(diào)試的難度 。在傳統(tǒng)的鎖編程中,由于鎖的存在,線程的執(zhí)行順序相對(duì)容易跟蹤,開(kāi)發(fā)者可以通過(guò)在關(guān)鍵代碼處添加日志,清晰地了解線程的執(zhí)行流程 。但在無(wú)鎖編程中,多個(gè)線程可能同時(shí)對(duì)共享資源進(jìn)行操作,線程之間的執(zhí)行順序交錯(cuò)復(fù)雜,很難通過(guò)簡(jiǎn)單的日志來(lái)跟蹤每個(gè)線程的具體操作和執(zhí)行順序 。
為了應(yīng)對(duì)這些調(diào)試?yán)щy,可以借助一些專(zhuān)業(yè)的調(diào)試工具 。在 Java 開(kāi)發(fā)中,Java Mission Control就是一個(gè)強(qiáng)大的調(diào)試工具,它可以監(jiān)控和分析 Java 應(yīng)用程序的性能和運(yùn)行狀態(tài),幫助開(kāi)發(fā)者查看線程的執(zhí)行情況、內(nèi)存使用情況等 。通過(guò)該工具,能夠直觀地看到各個(gè)線程在不同時(shí)間點(diǎn)的狀態(tài)和執(zhí)行的代碼片段,有助于定位無(wú)鎖編程中線程競(jìng)爭(zhēng)和數(shù)據(jù)不一致等問(wèn)題 。詳細(xì)的日志分析也是一種有效的策略 。在無(wú)鎖代碼中,合理地添加日志,記錄關(guān)鍵操作的執(zhí)行時(shí)間、操作前后的變量值以及線程的 ID 等信息,通過(guò)對(duì)這些日志的分析,可以更深入地了解程序的執(zhí)行過(guò)程,從而發(fā)現(xiàn)潛在的問(wèn)題 。