對象引用的正確性在多線程環境下是一個複雜的問題,請參考,處理由引用計數引發的泄漏.簡單來講,咱們應該儘可能減小使用強引用,不然將有可能產生[處理由引用計數引發的泄漏]一文中描述的難以察覺的內存泄漏問題.也就是說,大多數狀況下咱們應該使用一個弱引用來指代一個對象,當咱們真正須要訪問這個對象的時候纔將其轉換成實際的對象.因此能夠弱引用理解爲一種handle,它只是底層對象表示的一層間接引用.html
能夠考慮以下場景:算法
咱們設計了一種網絡庫,分爲IO層和邏輯層,IO層管理實際的socket對象,而邏輯層能看到的只是socket的一個handle.邏輯層須要發送數據的時候,將handle和數據一塊兒打包交給IO層,IO層把handle轉化成實際的socket對象並完成數據發送.那麼問題來了,若是在IO層收到一個發送請求時,那個handle對應的socket實際上已經銷燬,那麼對handle的轉換就應該反映出這種狀況,讓轉換返回一個空指針.網絡
由於在[處理由引用計數引發的泄漏]描述的算法中,refobj *cast2refobj(ident _ident);
和atomic_32_t refobj_dec(refobj *r);
兩個方法是相當重要,而且實現相對複雜,因此本文主要目的就是介紹這兩個函數的做用及其正確性.多線程
咱們首先來看下refobj_dex
:socket
atomic_32_t refobj_dec(refobj *r) { atomic_32_t count; int c; struct timespec ts; assert(r->refcount > 0); if((count = ATOMIC_DECREASE(&r->refcount)) == 0){ r->identity = 0; c = 0; for(;;){ if(COMPARE_AND_SWAP(&r->flag,0,1)) break; if(c < 4000){ ++c; __asm__("pause"); }else{ ts.tv_sec = 0; ts.tv_nsec = 500000; nanosleep(&ts, NULL); } } r->destructor(r); } return count; }
關鍵部分在引用計數爲0,要準備銷燬對象的分支.首先將對象的identity置0,而後在一個for循環中對嘗試flag變量置1,只有當設置成功纔會退出循環執行最後的析構函數.這裏的主要迷惑之一是for循環和flag變量的做用是什麼.讓咱們先看下cast2refobj
的實如今回來討論;ide
refobj *cast2refobj(ident _ident) { refobj *ptr = NULL; if(!_ident.ptr) return NULL; TRY{ refobj *o = (refobj*)_ident.ptr; do{ atomic_64_t identity = o->identity; if(_ident.identity == identity){ if(COMPARE_AND_SWAP(&o->flag,0,1)){ identity = o->identity; if(_ident.identity == identity){ if(refobj_inc(o) > 1) ptr = o; else ATOMIC_DECREASE(&o->refcount); } o->flag = 0; break; } }else break; }while(1); }CATCH_ALL{ ptr = NULL; }ENDTRY; return ptr; }
cast2refobj
的做用就是將一個handle轉換成對象,若是對象未被銷燬返回對象,不然返回NULL.在do循環中,首先判斷handle保存的identity與實際對象的是否一致,若是不一致代表handle中存放的對象確定已經不是原來的對象了,因此返回NULL.而當identity一致的時候,首先作的第一件事又是對flag置1.可見這個flag是這個算法的重點.如今咱們來討論flag的做用.函數
flag主要由兩個做用:atom
1) 防止多個線程同時進入cast2refobj
的核心部分,讓咱們考慮如下場景:線程
有A,B,C3個線程,A線程執行refobj_dec
,在成功執行if((count = ATOMIC_DECREASE(&r->refcount)) == 0)
以後,r->identity = 0;
以前暫停.B,C則幾乎同時執行cast2refobj
,這個時候由於identity還未被清0,因此B,C看到的identity必然與其持有的handle的保持一致,若是沒有if(COMPARE_AND_SWAP(&o->flag,0,1))
這行代碼咱們看看會發生什麼事情.假設B線程先執行, 它執行if(refobj_inc(o) > 1)的時候返回值應該是1,那麼條件判斷失敗,沒有將ptr設置爲o,因此ptr依舊是NULL.但在執行完判斷在準備執行另外一個分支的ATOMIC_DECREASE(&o->refcount);
以前它也被暫停,那麼當C執行if(refobj_inc(o) > 1)
它會進入ptr=o
的分支(由於refobj_inc(o)會返回2),也就是說,轉換成功,而實際上返回的倒是一個正準備銷燬的對象.flag就是爲了防止這種狀況的發生,它使得多個線程執行cast2refobj
的時候,只能互斥的進入if(refobj_inc(o) > 1)
.設計
2) 讓r->destructor
延後執行,使得執行cast2refobj
並已經進入if(COMPARE_AND_SWAP(&o->flag,0,1))
內部的線程先退出cast2refobj
,而後再執行 r->destructor
.
另外還要注意的是cast2refobj
是被TRY CATCH所保護的,這樣作的緣由在於,在內存壓力大的狀況下,被銷燬對象的內存可能馬上歸還給系統,那麼對對象的訪問將會產生訪問異常.咱們必須捕獲這個異常,同時讓函數返回NULL(異常出現代表handle持有的對象一定是非法的了).