スレッドの安全なhandle


オブジェクト参照の正確性は、マルチスレッド環境において複雑な問題である、参照、参照カウントによる漏洩の処理.簡単に言えば、強いリファレンスの使用をできるだけ減らすべきであり、そうでなければ、「リファレンスカウントによる漏洩の処理」という文で説明する気づきにくいメモリ漏洩の問題が発生する可能性がある.すなわち、ほとんどの場合、弱いインデックスを使用してオブジェクトを指すべきであり、本当にこのオブジェクトにアクセスする必要がある場合に実際のオブジェクトに変換する必要がある.したがって、弱い参照はhandleと理解することができ、それは下層オブジェクトが表す間接参照にすぎない.
次のようなシーンが考えられます.
我々はネットワークライブラリを設計し,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);の2つの方法が重要であり、比較的複雑であるため、本明細書の主な目的は、この2つの関数の役割とその正確性を紹介することである.
まずrefobj_dexを見てみましょう
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の実現を見てみましょう.
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は主に2つの役割を果たします.
1)cast2refobjのコア部分に複数のスレッドが同時に進入することを防止し、以下のシナリオを考えてみましょう.
A,B,Cの3つのスレッドがある、Aスレッドはrefobj_decを実行し、if((count = ATOMIC_DECREASE(&r->refcount)) == 0)の実行に成功した後、r->identity = 0;の前に一時停止する.B,Cはほぼ同時にcast2refobjを実行するが、このときidentityはまだクリアされていないため、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が持っているオブジェクトが必ず不正であることを示しています).