无锁编程技术及实现「建议收藏」
1.基于鎖的編程的缺點
多線程編程是多CPU系統在中應用最廣泛的一種編程方式,在傳統的多線程編程中,多線程之間一般用各種鎖的機制來保證正確的對共享資源(shareresources)進行訪問和操作。
在多線程編程中只要需要共享某些數據,就應當將對它的訪問串行化。比如像++count(count是整型變量)這樣的簡單操作也得加鎖,因為即便是增量操作這樣的操作,,實際上也是分三步進行的:讀、改、寫(回)。
movlx,%eax
addl$1,%eax
movl%eax,x
更進一步,甚至內存變量的賦值操作都不能保證是原子的,比如在32位環境下運行這樣的函數
voidsetValue()
{
value=0x100000006;
}
執行的過程中,這兩條指令之間也是可以被打斷的,而不是一條原子操作。(也就是所謂的寫撕裂)
所以修改共享數據的操作必須以原子操作的形式出現,這樣才能保證沒有其它線程能在中途插一腳來破壞相應數據。
而在使用鎖機制的過程中,即便在鎖的粒度(granularity),負載(overhead),競爭(contention),死鎖(deadlock)等需要重點控制的方面解決的很好,也無法徹底避免這種機制的如下一些缺點:
1,鎖機制會引起線程的阻塞(block),對于沒有能占用到鎖的線程或者進程,將一直等待到鎖的占有者釋放鎖資源后才能繼續執行,而等待時間理論上是不可設置和預估的。
2,申請和釋放鎖的操作,增加了很多訪問共享資源的消耗,尤其是在鎖競爭(lock-contention)很嚴重的時候,比如這篇文章所說:http://preshing.com/20111118/locks-arent-slow-lock-contention-is/
3,現有實現的各種鎖機制,都不能很好的避免編程開發者設計實現的程序出現死鎖或者活鎖的可能
4,優先級反轉(prorithyinversion)和鎖護送(Convoying)的現象
5,難以調試
無鎖編程(Lock-Free)就是在某些應用場景和領域下解決以上基于鎖機制的并發編程的一種方案。
2.無鎖編程(LOCK-FREE)的定義
提到無鎖編程(lock-free),按字面最直觀的理解是不使用鎖的情況下實現多線程之間對變量同步和訪問的一種程序設計實現方案。嚴格的說這個理解是不對的,Lock-Free的程序肯定是不包括鎖機制的,而不包括鎖機制的程序不一定是lock-free的。
更準確的說,在并發編程上按照同步的維護劃分,可以分為阻塞的編程方式(Block)和非阻塞的編程方式(Non-blockingSynchronization)。阻塞的編程方式基本是基于鎖的(lock-based)。其中無鎖編程(Lock-free)屬于非阻塞同步(Non-blockingSynchronization)中的一種情況,實現非阻塞同步的算法方案按照效果要求不同可以粗略的分為:
Wait-free:滿足等待無關的程序,任何線程可以在有限步之內結束,不管其它線程的執行速度和進度如何
Lock-free:鎖無關的程序,一個鎖無關的程序能夠確保它所有線程中至少有一個能夠繼續往下執行,而有些線程可能會被的延遲。然而在整體上,在某個時刻至少有一個線程能夠執行下去。作為整體進程總是在前進的,盡管有些線程的進度可能沒有其它線程進行的快。
Obstruction-free:在任何時間點,一個孤立運行線程的每一個操作可以在有限步之內結束。只要沒有競爭,線程就可以持續運行。一旦共享數據被修改,Obstruction-free要求中止已經完成的部分操作進行回滾。
更細致的還可以把并發編程按效果劃分為:
Blocking
1.Blocking
2.Starvation-Free
Obstruction-Free
3.Obstruction-Free
Lock-Free
4.Lock-Free(LF)
Wait-Free
5.Wait-Free(WF)
6.Wait-FreeBounded(WFB)
7.Wait-FreePopulationOblivious(WFPO)
具體細節可以參考這篇文章http://ifeve.com/lock-free-and-wait-free/,有對阻塞非阻塞的等定義的詳細描述,這里不詳細論述。
3.無鎖編程中涉及的一些技術原理
無鎖編程具體使用和考慮到的技術方法包括:原子操作(atomicoperations),內存柵欄(memorybarriers),內存順序沖突(memoryorder),指令序列一致性(sequentialconsistency)和順ABA現象等等,這方面借用一篇資料的總結的圖概況:
在這其中最基礎最重要的是操作的原子性或說原子操作。原子操作可以理解為在執行完畢之前不會被任何其它任務或事件中斷的一系列操作。原子操作是非阻塞編程最核心基本的部分,沒有原子操作的話,操作會因為中斷異常等各種原因引起數據狀態的不一致從而影響到程序的正確。
對于原子操作的實現機制,在硬件層面上CPU處理器會默認保證基本的內存操作的原子性,CPU保證從系統內存當中讀取或者寫入一個字節的行為肯定是原子的,當一個處理器讀取一個字節時,其他CPU處理器不能訪問這個字節的內存地址。但是對于復雜的內存操作CPU處理器不能自動保證其原子性,比如跨總線寬度或者跨多個緩存行(CacheLine),跨頁表的訪問等。這個時候就需要用到CPU指令集中設計的原子操作指令,現在大部分CPU指令集都會支持一系列的原子操作。而在無鎖編程中經常用到的原子操作是Read-Modify-Write(RMW)這種類型的,這其中最常用的原子操作又是COMPAREANDSWAP(CAS),幾乎所有的CPU指令集都支持CAS的原子操作,比如X86平臺下中的是CMPXCHG。
繼續說一下CAS,CAS操作行為是比較某個內存地址處的內容是否和期望值一致,如果一致則將該地址處的數值替換為一個新值。CAS能夠操作的位數越多,使用它來實現鎖無關的數據結構就越容易(細節可以在intel手冊中查看)。CAS操作具體的實現原理主要是兩種方式:總線鎖定和緩存鎖定。所謂總線鎖定,就是CPU執行某條指令的時候先鎖住數據總線的,使用同一條數據總線的CPU就無法訪問內存了,在指令執行完成后再釋放鎖住的數據總線。鎖住數據總線的方式系統開銷很大,限制了訪問內存的效率,所以又有了基于CPU緩存一致性來保持操作原子性作的方法作為補充,簡單來說就是用CPU的緩存一致性的機制來防止內存區域的數據被兩個以上的處理器修改(可詳見CPU緩存的MESI協議)。
在操作系統的層面,Linux系統提供了軟件級的原子操作,包括兩大類系統調用,一類是基于對整數進行操作的atomic_set/and/inc,一類是針對單獨的位進行操作的set/clear/change_bit,它們大部分都是基于硬件層面的CAS的指令實現的。
在各種開發語言中(c,c++,java)基于操作系統提供的接口也都封裝實現了對應的原子操作api,所以開發者完全可以直接調用各個開發語言提供的接口實現無鎖程序。
除了使用原子操作保證操作的原子性,還要考慮在不用的語言和內存模型下,如何保證,操作的順序性,編譯時和運行時的指令重排序,還有由假共享引起內存順序沖突(Memoryorderviolation)等等細節。原子性確保指令執行期間不被打斷,要么全部執行,要么根本不執行,而順序性確保即使兩條或多條指令出現在獨立的執行線程中或者獨立的處理器上時,保持它們本該執行的順序。假共享是指多個CPU同時修改同一個緩存行的不同部分而引起其中一個CPU的操作無效,當出現這個內存順序沖突時,CPU必須清空流水線。這些其實在多核編程的環境下的locked-base的實現中也有類似的問題所以這里就不多展開。
4.幾個例子
JohnD.Valois《ImplementingLock-FreeQueues》中提到的一個基于鏈表的無鎖隊列鏈表的實現,可以作為了解lock-free一個例子
EnQueue(x){//入隊列方法
q=newrecord();
q->value=x;//隊列節點的值
q->next=NULL;//下一個節點
p=tail;//保存尾節點指針
oldp=p;
do{//開始loopcas
while(p->next!=NULL)//用來防止進行cas(tail,oldp,q)操作的線程掛掉引起死循環
p=p->next;
}while(CAS(p.next,NULL,q)!=TRUE);
CAS(tail,oldp,q);
}
DeQueue()//出隊列方法
{
do{
p=head;
if(p->next==NULL){
returnERR_EMPTY_QUEUE;
}
while(CAS(head,p,p->next)!=TRUE);
returnp->next->value;
}
具體實現中,在linux平臺上gcc編譯器(內置了支持cas操作的函數,或者直接調用匯編命令(比如CMPXCHG和CMPXCHG8B)也可以在c程序里實現cas操作。GCC(GNUCompilerCollection,4.1和更高版本)提供幾個內置函數,可以使用它們在x86和x86-64平臺上實現CAS操作,這些內置函數包括:
type__sync_fetch_and_add(type*ptr,typevalue)
type__sync_fetch_and_sub(type*ptr,typevalue)
type__sync_fetch_and_or(type*ptr,typevalue)
type__sync_fetch_and_and(type*ptr,typevalue)
type__sync_fetch_and_xor(type*ptr,typevalue)
type__sync_fetch_and_nand(type*ptr,typevalue)
type__sync_add_and_fetch(type*ptr,typevalue)
type__sync_sub_and_fetch(type*ptr,typevalue)
type__sync_or_and_fetch(type*ptr,typevalue)
type__sync_and_and_fetch(type*ptr,typevalue)
type__sync_xor_and_fetch(type*ptr,typevalue)
type__sync_nand_and_fetch(type*ptr,typevalue)
bool__sync_bool_compare_and_swap(type*ptr,typeoldvaltypenewval,…)
type__sync_val_compare_and_swap(type*ptr,typeoldvaltypenewval,…)
type__sync_lock_test_and_set(type*ptr,typevalue,…)
更多細節分析可參看JohnD.Valois的《ImplementingLock-FreeQueues》
同樣,在java語言中Lock-Free的數據結構和算法其實更加常見,在java.util.concurrent包中大量實現都是建立在基于CAS實現Lock-Free算法上,沒有CAS就不會有此包。Java.util.concurrent.atomic提供了基于CAS實現的若干原語:
AtomicBoolean—原子布爾
AtomicInteger—原子整型
AtomicIntegerArray—原子整型數組
AtomicLong—原子長整型
AtomicLongArray—原子長整型數組
AtomicReference—原子引用
AtomicReferenceArray—原子引用數組
AtomicMarkableReference—原子標記引用
AtomicStampedReference—原子戳記引用
AtomicIntegerFieldUpdater—用來包裹對整形volatile域的原子操作
AtomicLongFieldUpdater—用來包裹對長整型volatile域的原子操作
AtomicReferenceFieldUpdater—用來包裹對對象volatile域的原子操作
引入這些類的主要目的就是為了實現Lock-Free算法和相關數據結構,以incrementAndGet這個方法為例:
publicfinalintincrementAndGet(){
for(;;){
intcurrent=get();
intnext=current+1;
if(compareAndSet(current,next))
returnnext;
}
}
在這里使用CAS原子操作,每次讀取數據數值后將此數值和+1后的結果進行CAS操作,如果成功就返回結果,否則重試到成功為止。其中compareAndSet是java中實現的CAS函數,在java語言中的實現,是借助JNI機制來調用匯編實現的:
publicfinalbooleancompareAndSet(intexpect,intupdate){
returnunsafe.compareAndSwapInt(this,valueOffset,expect,update);
}
unsafe.compareAndSwapInt是個本地方法調用,對應的x86處理器的jni源碼
#defineLOCK_IF_MP(mp)__asmcmpmp,0\
__asmjeL0\
__asm_emit0xF0\
__asmL0:
inlinejintAtomic::cmpxchg(jintexchange_value,volatilejint*dest,jintcompare_value){
//alternativeforInterlockedCompareExchange
intmp=os::is_MP();
__asm{
movedx,dest
movecx,exchange_value
moveax,compare_value
LOCK_IF_MP(mp)//判斷是否是多核,是則添加LOCK指令維護順序一致性
cmpxchgdwordptr[edx],ecx
}
}
再看看在java里無鎖隊列的實現作為對比參照:
importjava.util.concurrent.atomic.AtomicReference;
publicclassLockFreeQueue<T>{
privateAtomicReference<Node>head;//可以規避ABA現象
privateAtomicReference<Node>tail;
/**
*Createanewobjectofthisclass.
*/
publicLockFreeQueue(){
Nodesentinel=newNode(null);
head=newAtomicReference<Node>(sentinel);
tail=newAtomicReference<Node>(sentinel);
}
/**
*Enqueueanitem.
*@paramvalueItemtoenqueue.
*/
publicvoidenq(Tvalue){
//trytoallocatenewnodefromlocalpool
Nodenode=newNode(value);
while(true){
Nodelast=tail.get();
Nodenext=last.next.get();
//aretheyconsistent
if(last==tail.get()){
if(next==null){
//trytolinknodetoendoflist
if(laspareAndSet(next,node){
//enqdone,trytoadvancetail
tail.compareAndSet(last,node);
return;
}
}else{
//trytoswingtailtonextnode
tail.compareAndSet(last,next);
}
}
}
}
/**
*Dequeueanitem.
*@throwsqueue.EmptyExceptionThequeueisempty.
*@returnItemattheheadofthequeue.
*/
publicTdeq()throwsEmptyException{
while(true){
Nodefirst=head.get();
Nodelast=tail.get();
Nodenext=first.next.get();
//aretheyconsistent
if(first==head.get()){
if(first==last){
if(next==null){
thrownewEmptyException();
}
//tailisbehind,trytoadvance
tail.compareAndSet(last,next);
}else{
Tvalue=next.value;
if(head.compareAndSet(first,next)){
returnvalue;
}
}
}
}
}
/**
*Itemsarekeptinalistofnodes.
*/
publicclassNode{
/**
*Itemkeptbythisnode.
*/
publicTvalue;
/**
*Nextnodeinthequeue.
*/
publicAtomicReference<Node>next;
/**
*Createanewnode.
*/
publicNode(Tvalue){
this.next=newAtomicReference<Node>(null);
this.value=value;
}
}
最后這里隨便說一下CAS操作的ABA的問題,所謂的ABA的問題簡要的說就是,線程a先讀取了要對比的值v后,被線程b搶占了,線程b對v進行了修改后又改會v原來的值,線程1繼續運行執行CAS操作的時候,無法判斷出v的值被改過又改回來。ABA對基于指針實現的算法影響很大。上面的C語言里實現的程序是有可能被ABA問題影響的,因為它的CAS操作判斷的是指針的地址,這個地址被重用的可能性很大(地址被重用是很經常發生的,一個內存分配后釋放了,再分配,很有可能還是原來的地址,內存管理中重用內存基本上是一種很常見的行為)。解決ABA的問題的一種方法是,一次用CAS檢查雙倍長度的值,前半部是指針,后半部分是一個計數器;或者對CAS的數值加上版本號?!禝mplementingLock-FreeQueues》也有提到相關的解決方案。
5.結論和建議
無鎖編程方式相對基于鎖的編程方式,具備一定的優點,比如不會發生死鎖,不會有優先級倒置,進行CAS操作的消耗比加鎖操作輕很多等等。單從這個角度上講在對應用程序不太復雜,而對操作實時性要求較高時,采用無鎖多線程能發揮一定優勢。
在性能上基于CAS實現的硬件級的互斥,其單次操作性能比相同條件下的應用層的較為高效,但當多個線程并發時,硬件級的互斥引入的消耗一樣很高(類似spin_lock)。無鎖算法及相關數據結構并不意味在所有的環境下都能帶來整體性能的極大提升。循環CAS操作對時會大量占用cpu,對系統時間的開銷也是很大。這也是基于循環CAS實現的各種自旋鎖不適合做操作和等待時間太長的并發操作的原因。而通過對有鎖程序進行合理的設計和優化,在很多的場景下更容易使程序實現高度的并發性。
在開發維護的成本和復雜度上,無鎖編程難度非常高,類似ABA的問題也比較難直觀的探測和解決,并且實現細節和硬件平臺相關性很強。目前理論和實踐上只有少數數據結構可以支持實現無鎖編程,比如隊列、棧、鏈表、詞典等,目前要在產品業務開發環境中進行大規模的無鎖編程較為困難,更多的是用在部分特殊場景解決鎖競爭等問題比較合適,比如操作系統中實現metux,semaphare,一些語言的庫的實現(比如javacurrentlib,lmaxdisprute)。若想在應用開發中嘗試的Lock-Free的方案,建議可以選擇合適的第三方lib實現。
附一. 些常見的相關lib庫工具
https://github.com/mthssdrbrg/LockFreeStack
http://www.lmax.com/disruptor
http://www.cse.chalmers.se/research/group/noble/
http://www.cl.cam.ac.uk/research/srg/netos/lock-free/
http://www.rossbencina.com/code/lockfree
http://www.liblfds.org/
http://concurrencykit.org/
http://www.cl.cam.ac.uk/research/srg/netos/lock-free/
附二. 參考資料和進階閱讀;
WritingLock-FreeCode:ACorrectedQueue
TheTroubleWithLocks
Intel64andIA-32ArchitecturesSoftwareDeveloper’sManual
TheArtofMultiprocessorProgramming
ImplementingLock-FreeQueues
Lock-FreeTechniquesforConcurrentAccesstoSharedObjects
Lock-FreeLinkedListsUsingCompare-and-Swap
Yetanotherimplementationofalock-freecirculararrayqueue
CAS-BasedLock-FreeAlgorithmforSharedDeques
http://en.wikipedia.org/wiki/Compare-and-swap
https://gcc.gnu.org/onlinedocs/gcc-4.3.5/gcc/Atomic-Builtins.html
http://www.intel.com/Assets/en_US/PDF/manual/253666.pdf
http://en.cppreference.com/w/cpp/atomic
http://en.cppreference.com/w/c/atomic
http://en.wikipedia.org/wiki/Load-Link/Store-Conditional
http://en.wikipedia.org/wiki/Memory_barrier
http://en.wikipedia.org/wiki/ABA_problem
http://en.wikipedia.org/wiki/Non-blocking_algorithm
http://www.ibm.com/developerworks/cn/linux/l-cn-lockfree/
http://en.wikipedia.org/wiki/Optimistic_concurrency_control
http://gcc.gnu.org/onlinedocs/gcc-4.1.0/gcc/Atomic-Builtins.html
總結
以上是生活随笔為你收集整理的无锁编程技术及实现「建议收藏」的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 服务端性能_python
- 下一篇: qt打包rpm时候先安装其他软件_云计算