一種線程安全的handle

對象引用的正確性在多線程環境下是一個複雜的問題,請參考,處理由引用計數引發的泄漏.簡單來講,咱們應該儘可能減小使用強引用,不然將有可能產生[處理由引用計數引發的泄漏]一文中描述的難以察覺的內存泄漏問題.也就是說,大多數狀況下咱們應該使用一個弱引用來指代一個對象,當咱們真正須要訪問這個對象的時候纔將其轉換成實際的對象.因此能夠弱引用理解爲一種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持有的對象一定是非法的了).

相關文章
相關標籤/搜索