【読書ノート】linuxシステムはsemaphoreで古典的な生産者-消費者問題を解決する

6389 ワード

Linuxシステムでマルチプロセスまたはマルチスレッドの同時プログラミングを処理する場合、プロセス/スレッド同期はよく発生する問題です.多くの同期問題のシナリオでは、生産者-消費者問題(Producer-Consumer Problem)は、同期問題に関連するほとんどの古典的な教材で言及される古典的なモデルである.linuxシステムでは、同期を実現する典型的な考え方は、カーネルによって提供される3つの変数であり、それぞれ:1)反発量(mutex);2)信号量(semaphore);3)条件変数(condition variable).本明細書の次の内容は、「Computer Systems:A Programmer's Perspective」の12.5節である.信号量でスレッドを同期させる読書ノートは、semaphoreを利用して生産者-消費者の同期問題をどのように解決するかを説明することを目的としている.            1. 信号量semaphoreの意味と用途semaphoreの概念は有名なトゥーリン賞受賞者であるオランダCS科学者Edsger Dijkstra(semaphoreを提出する前に、Dijkstra's algorithmが図検索における最短経路問題を解決したことで知られている)が1965年に提出した.簡単に言えば、semaphoreは特殊なタイプの変数で、非負の整数値を有し、2つの特殊な操作をサポートしています.この2つの操作はPとV:P(s):sがゼロでない場合、Pはsを1減らし、すぐに戻ります.sがゼロの場合、P(s)を呼び出すこのスレッドは、sがゼロでないまで保留され、Vオペレーションがこのスレッドを再起動します.再起動後、P操作はsを1減算し、呼び出し元に制御を返す.V(s):V操作でsを1加算します.いずれかのスレッドブロックがPオペレーション待機sでゼロでない場合、Vオペレーションはこれらのスレッドの1つを再起動し、スレッドはsを1減少させ、そのPオペレーションを完了する.注:P/Vはオランダ語Proberen(テスト)とVerhogen(増加)に由来します.注意すべき点:1)Pにおけるテストとマイナス1操作は分割できない,すなわち予測信号量sが非ゼロになると,sをマイナス1にし,中断することができない.Vにおけるプラス1操作も分割できない.すなわち、信号量のロード、プラス1、および記憶中に割り込みがあってはならない.すなわち、P/Vはいずれも原子操作である.2)Vの定義には待機スレッドが再起動される順序は定義されておらず,Vは待機中のスレッドを1つしか再起動できないことが唯一の要件である.したがって、複数のスレッドが同じ信号量を待っている場合、V操作がどのスレッドを再起動するか予測できません.3)2)から分かるように、複数のスレッドがP動作でブロックされている場合、他のスレッドのV動作はそのうちの1つのみを「起動」することができ、これは、驚くべき群効果(Thundering Herd Problem)を回避するためにカーネルの実装を保証する必要がある.linuxカーネルには、P/V原語に対応する具体的な関数が提供する、具体的には. semaphoreは、共有変数への反発アクセスを確保するための便利な方法を提供し、基本的な考え方は、各共有変数(または関連する共有変数のセット)を1つの信号量s(初期は1)に関連付け、その後、P(s)およびV(s)操作で対応する臨界領域を保護することである.このようにして共有変数を保護する信号量は、常に0または1の値であるため、二元信号量(binary semaphore)と呼ばれる.さらに、信号量は、カウント信号量(counting semaphore)と呼ばれる利用可能なリソースのセットをカウントするために使用されてもよい.
  
        2. 生産者-消費者問題の典型的な生産者-消費者問題を下図に示す.生産者と消費者スレッドはn個のスロットからの有限バッファを共有し、生産者スレッドは新しいitemを繰り返し生成し、バッファの末尾に挿入し、消費者スレッドはバッファの頭部からこれらのitemを取り出し、消費する.                       
itemの挿入と取り出しは共有変数の更新に関連するため、バッファへのアクセスが反発することを保証する必要があります.しかし、反発アクセスのみを保証するには十分ではありません.バッファへのアクセスをスケジュールする必要があります.バッファがいっぱいである場合(空のslotがない場合)、生産者はslotが利用可能になるまで待たなければなりません.同様に、バッファが空(使用可能なitemがない)である場合、消費者は、1つのitemが使用可能になるまで待たなければならない.
    3. Producer-Consumerの問題をSemaphoreで解決するには、次のカスタム変数タイプを使用して、生産者-消費者モデルを抽象化します.  
    typedef struct 
    {
        int * buf;     // buffer array
        int   n;       // maximum number of slots
        int   front;   // buf[(front+1) % n] is first item
        int   rear;    // buf[rear % n] is last item
        sem_t mutex;   // protects accesses to buf
        sem_t slots;   // counts available slots
        sem_t items;   // counts available items
    } sbuf_t;

上からsbuf_tタイプ定義から,最大n個のitemsを配置できる有限バッファbufを維持し,frontとrearはそれぞれbufの最初のitemと最後のitemを指し,3つの信号量はバッファへのアクセスを同期し,mutex信号量は反発するバッファアクセスを提供し,slotsとitemsはそれぞれ空きスロットビットと利用可能なitemsをカウントした.次に、sbuf_をそれぞれ定義します.init()、sbuf_deinit()、sbuf_Insert()とsbuf_remove()は生産者-消費者問題をシミュレートする.注意:紙面に限られ、以下に示すサンプルコードでは異常状況は処理されず、実際の符号化実装では異常処理は無視できない.
    // create an empty, bounded, shared FIFO buffer with n slots
    void sbuf_init(sbuf_t * sp, int n)
    {
        sp->buf = calloc(n, sizeof(int));   
        sp->n   = n;                        // buffer holds max of n items
        sp->front = sp->rear = 0;           // empty buffer if front == rear
        sem_init(&sp->mutex, 0, 1);         // binary semaphore for mutex-locking
        sem_init(&sp->slots, 0, n);         // initially, buf has n empty slots 
        sem_init(&sp->items, 0, 0);         // initially, buf has zero data items
    }
の上の関数では、有限バッファにheap上に空間を割り当て、frontとrearを設定して空のバッファであることを示し、3つの信号量に初期値を付与します.この関数を呼び出した後、保護された初期空の有限バッファを作成しました.   
    // clean up buffer sp
    void sbuf_deinit(sbuf_t * sp)
    {
        free(sp->buf);
    }
    // insert item onto the rear of shared buffer sp, it's an abstract of producer
    void sbuf_insert(sbuf_t * sp, int item)
    {
         P(&sp->slots);                            // wait for available slot
         P(&sp->mutex);                            // lock the buffer
         sp->buf[(++sp->rear) % (sp->n)] = item;   // insert the item
         V(&sp->mutex);                            // unlock the buffer
         V(&sp->items);                            // announce available ite
    }
    // remove and return the first item from buffer sp,  it's an abstract of consumer
    void sbuf_remove(sbuf_t * sp)
    {
         int item;
         P(&sp->items);                             // wait for available item
         P(&sp->mutex);                             // lock the buffer
         item = sp->buf[(++sp->front) % (sp->n)];   // remove the item
         V(&sp->mutex);                             // unlock the buffer
         V(&sp->slots);                             // announce available slot
         return item;  
    }
       
上記のコードについて説明する必要があるのは、次のとおりです.
有限バッファbufは
circular bufferで用いられる(bufにアクセスするindex計算式からこの点がわかる)ため、最初のitemをどこから書くかは重要ではない(したがって、コードの最初のitemはbuf[1]の位置に書かれている).また,コードから分かるように,ここのcircular bufferは空でも満でもfront==rearである.このような判空/判満条件を採用するメリットはslotを無駄にしないことであり、欠点は空/満条件がfront==rearであり、迷いやすくバグを導入することである.circular bufferが他の方法で空/満を判定する考え方については、本ノートの議論の範囲内ではなく、興味のある学生は、ここを参考にすることができます.
       
思考問題:
pは生産者数,cは消費者数,nはバッファ内で最も多いitems数とする.次のシーンでは、sbuf_を指定します.Insertとsbuf_removeでの反発ロック信号量が必要かどうか.
            A. p = 1, c = 1, n > 1
            B. p = 1, c = 1, n = 1
            C. p > 1, c > 1, n = 1
       
Answers:
Aシーンでは、mutex信号量が必要です.producerとconsumerはバッファに同時にアクセスするからです.
B/Cシーンでは、mutexは必須ではありません.bufferは1つのslotしかなく、空ではなく満タンであるため、producerはitemを生産した後、buffer満はP(&sp->slots)に閉塞する.一方、consumerは、このitemのみを消費し、V(&sp->slots)を介してproducerを起動しようとすると、bufferに使用可能なitemがある前に、bufferが空であるためP(&sp->items)にブロックされます.n=1の場合、生産者および消費者は、sp->slotsおよびsp->itemsを用いて反発アクセスバッファを実現することができ、したがって、この場合、明示的な反発ロックを省くことができる.
【参考資料】
1. . chapter 12.5 2. wikipedia: Producer-Consumer Problem  3. wikipedia: semaphore  4. wikipedia: Thundering Herd Problem 
5. wikipedia: Circular buffer
 
================= EOF ================