キャプティブフリーアルゴリズム:モデル「do、write、(entrust to another)」

habrapublicのアドバイスに従い、「 ロックフリー 」という用語の翻訳の新しいバージョンを試しています



前回 、「スピリットレス」アルゴリズムを見て、キャプチャが実装されたため、キャプチャされたデータにアクセスするスレッドはリリースを待たずに「回避策」で送信されます(キャッシュサービスを使用せずに必要な結果を計算します)。 レイモンドは次の投稿で、「回避策」がない場合にこのアルゴリズムを改善する方法について説明しています。 ただし、アルゴリズムはキャプティブのままです。各スレッドは、キャプチャされたデータのリリースを待たずに機能し続けます。



シェア変数では、2つのサービスビットが必要になりました。前の例のように、キャプチャフラグに加えて、フラグ「commissioned new work」。 委任されたタスクが複雑な場合は、フラグに加えて、そのパラメーターを別の場所に保存する必要があります。 たとえば、一般変数では、パラメータを使用して(メモリ内で整列された)オブジェクトへのポインタを、ポインタの空き最下位ビット(2つの名前付きフラグ)に格納できます。



オブジェクトに対してアクションを実行する前に、まず、対応するフラグをアトミックに設定してキャプチャします。 オブジェクトが既にキャプチャされていることが判明した場合、2番目のフラグを設定して、キャプチャされたストリームにアクションの実行を委任します。



オブジェクトをキャプチャできた場合は、そのオブジェクトでの作業を完了した後、キャプチャフラグを削除すると同時に、新しいジョブを委託されたかどうかを確認します。 (つまり、オブジェクトをキャプチャしたままの間にオブジェクトへの呼び出しがあったかどうか。)作業があれば、それも実行します。 など、オブジェクトのロックを解除する1日まで、保留中の作業はありません。 オブジェクトを「キャプチャされていないが、作業があります」状態のままにする権利はありません。



結果の「do、write、(別への割り当て)」モデルは、あらゆる種類の障害が発生した場合の多くのサイクルから編まれています。 各アトミック操作はサイクルで構成されています。 遅延作業の実行はサイクルで実行されます。 また、 InterlockedCompareExchange



呼び出しで使用済みデータの上書きが明らかになるたびに、完了したすべての作業をロールバックし、最初から実行する必要があります。 モデルは非常に紛らわしいです。 レイモンドは次のように説明します。「全世界では、おそらく5つだけが正しく実装できますが、私はこれらの5つに属していません。」にもかかわらず、例として、彼は2つの操作をサポートするGroupWaitというオブジェクトの例を挙げます オブジェクトは、単にハンドルのリストとして実装されます。 トリッキーなスナップレスモデルがなくても実装できたことは明らかです。たとえば、Windows XP以降に実装されたリストに対してアトミック関数を使用します 。 タスクは、口頭での説明にサンプルコードを添付する場合にのみ選択されます。 GroupWaitをスレッドセーフリストを実装するためのモデルとして使用しないでください。



そのため、ポインターの下位2ビットを使用して2つのフラグを格納します。キャプチャフラグは、特定のスレッドがリストに対して操作を実行していることを示します。 リストがキャプチャされている間にユーザーがイベントのインストールを要求したことを示す作業フラグ。



 //警告! このコードを使用する場合、あなたは馬鹿です-上記のテキストを読んでください

 struct NODE;
 NODE *ノード(LONG_PTRキー){return reinterpret_cast <NODE *>(キー);  }

 enum {
 ロック済み= 1
 シグナル= 2
 };

 struct NODE {
  NODE * pnNext;
 ハンドルhEvent;

  LONG_PTRキー(){return reinterpret_cast <LONG_PTR>(this);  }
  NODE * Ptr(){Return Node(Key()&〜(Locked | Signaled));  }
 };

 #define NODE_INVALIDノード(-1)

クラスGroupWait {
公開:
  GroupWait():m_pnRoot(NULL){}
  〜GroupWait();
  BOOL AddWait(HANDLE hEvent);
  void SignalAll();
プライベート:
  NODE * m_pnRoot;
 };


リストアイテムへのポインターとフラグのペアを1つの値に組み合わせるため、使用済みの型を相互に変換するメソッドを定義することは有益Ptr



ポインターをPtr



Key



は数値をPtr



Ptr



使用可能なポインターをPtr



ます(低ビットのフラグなし) 。



p|S|L



形式のビットフィールドとして値を表します:p-リストの次の要素へのポインター。 S-作業フラグ。 Lはキャプチャフラグです。 セットフラグSは、リストのすべての要素を処理する必要があることを意味します。これは、以下から始まります。要素内ではなく、外向きの矢印でマークされていると想像してください。



例:

    m_pnRoot
   + -------- +-+-+
   |  * | 0 | 1 |
   + --- | ---- +-+-+
       |
       v
   + -------- +-+-+ --------- +
 A |  * | 1 |?|  hEvent1 |
   + --- | ---- +-+-+ --------- +
       |
       v
   + -------- +-+-+ --------- +
 B |  * |?|?|  hEvent2 |
   + --- | ---- +-+-+ --------- +
       |
       v
   + -------- +-+-+ --------- +
 C |  NULL |?|?|  hEvent3 |
   + -------- +-+-+ --------- +


描かれているのは、3つのハンドルを含むGroupWaitオブジェクトです。 リストの先頭にあるリセットフラグSは、hEvent1イベントを設定する必要がないことを意味します。 それどころか、要素Aに設定されたフラグSは、Aの後のすべての要素を巡回し、対応するイベント、つまりhEvent2とhEvent3を設定する必要があることを意味します。 特に、要素BおよびCのフラグSの値は重要ではありません。 これらの要素は、要素AのSフラグで必要とされるため、どのような場合でも処理されます。したがって、リストの最後の要素のSフラグの値はまったく重要ではありません。



Lフラグはリストの先頭でのみ使用されます。 他のすべての要素では重要ではありません。



これで準備は完了です。 リストに新しいハンドルを追加します。



 BOOL GroupWait :: AddWait(ハンドルhEvent)
 {
  NODE * pnInsert = new(nothrow)NODE;
  if(pnInsert == NULL)はFALSEを返します。
  pnInsert-> hEvent = hEvent;

  NODE * pn;
  NODE * pnNew;
  {
   pn = InterlockedReadAcquire(&m_pnRoot、NODE_INVALID);
   pnInsert-> pnNext = pn;
   pnNew = Node(pnInsert-> Key()|(pn-> Key()&Locked));
  } while(InterlockedCompareExchangeRelease(&m_pnRoot、pnNew、pn)!= pn);
  TRUEを返します。
 }


リストの先頭に新しい要素を追加し、Lフラグが古い要素から新しい要素に変更されることを確認します。そうしないと、別のスレッドがすでにリストをキャプチャしていることがわかり、誤ってリストを「解放」します。 追加されるアイテムのSフラグはクリアされます。新しいイベントを設定することを誰も要求していません。 よく知られているモデル「do、write、(try again)」に従って、リストの先頭に要素を追加します-誰もリストの後ろを変更していないことを確認します。 「ABA問題」は発生しないことに注意してくださいm_pnRoot



の変更されていない値が別のオブジェクトを指していても、オブジェクトのコンテンツではなく、ポインター自体のみを使用します。



AddWaitメソッドの単純さは、「do、write、(assign to another)」モデルでは珍しいことです。失敗した場合、委任するものはありません。すべての作業はメモリへの1回の書き込みで構成されます。 残りのメソッドは、この単純さのために支払われます。この単純さでは、リストがキャプチャされている間にリストに追加された要素の処理を提供する必要があります。



SignalAllメソッドは非常に複雑なため、部分的に読み取る方が適切です。

 void GroupWait :: SignalAll()
 {
  NODE * pnCapture;
  NODE * pnNew;
  {
   pnCapture = InterlockedReadAcquire(&m_pnRoot、NODE_INVALID);

   if(pnCapture-> Key()&Locked){
    pnNew = Node(pnCapture-> Key()| Signaled);
   } else {
    pnNew = Node(Locked);
   }
  } while(InterlockedCompareExchangeAcquire(&m_pnRoot、
                               pnNew、pnCapture)!= pnCapture);

  if(pnCapture-> Key()&Locked)return;

  ...


リストがキャプチャされる場合、私たちに必要なのは、頭にフラグSを設定することだけです。リストが空いている場合、リストのすべての要素をキャプチャすると同時に、ヘッドの代わりに「スタブ」 NULL|0|1



書き込みNULL|0|1



。 どちらの場合も、リストの先頭は「do、write、(try again)」モデルに従って書き換えられます。記録が成功するまで試行を繰り返します。



Sフラグを設定することにより、リストをキャプチャしたスレッドに作業を委任しました。 リストの先頭に設定されたSフラグは、先頭に続くリストのすべての要素を処理する必要があることを意味します。つまり、 通常、リストのすべての要素。 必要なものだけです。 リストをキャプチャしたスレッドは、リストがリリースされるとフラグをチェックし、作業を行います。



リストがキャプチャされなかった場合、アクションによってキャプチャしました。 盗まれたアイテムは他のスレッドには見えないため、異なるスレッドから同時にSignalAllを呼び出しても、複数のイベントは発生しません。

  ...
  NODE * pnNext;
  NODE * pn;
  for(pn = pnCapture-> Ptr(); pn; pn = pnNext){
   SetEvent(pn-> hEvent);
   pnNext = pn-> pnNext-> Ptr();
   pnを削除します。
  }
  ...


「盗まれた」リストのバイパスは、スレッドの安全性を心配することなく、非常に簡単な方法で実装されます。 要素ごとにイベントを設定し、要素を削除するだけです。 唯一の機能は、 Ptr



を呼び出して、次の要素へのポインターからフラグを削除することです。



次に、リストのロックを解除する必要があります。 開始するには:

  ...
  pnCapture = pnNew;
  ...


メソッドの最初で、 pnNew



m_pnRoot



に書き込みpnNew



た。この値がリストの先頭に保存されていれば、簡単に降りることができます。つまり、作業中にリストにアクセスする人はいません。

  ...
  for(;;){
   pnNew = Node(pnCapture-> Key()&〜Locked);
   if(InterlockedCompareExchangeRelease(&m_pnRoot、
                       pnNew、pnCapture)== pnCapture){
   帰る
   }
  ...


最初に、リストが変更されたかどうかを確認します。変更されていない場合は、単にブロックを解除して完了です。 それ以外の場合は、新しいサイクルを開始します。リストのキャプチャ中に蓄積されたすべての保留中の作業を実行する必要があります。

  ...
   pnCapture = InterlockedReadAcquire(&m_pnRoot、NODE_INVALID);

   NODE * pnNew = Node(pnCapture-> Key()&〜(Locked | Signaled));
  ノード** ppn =&pnNew;
   NODE * pn;
   NODE * pnNext;

   BOOL fSignalSeen = FALSE;
   for(pn = pnNew; pn-> Ptr(); pn = pnNext){
    pnNext = pn-> Ptr()-> pnNext;
    if(fSignalSeen){
     SetEvent(pn-> Ptr()-> hEvent);
     delete pn-> Ptr();
    } else if(pn-> Key()&Signaled){
     fSignalSeen = TRUE;
     (* ppn)=ノード(ロック);  //盗み、キャプチャしたままにします
     SetEvent(pn-> Ptr()-> hEvent);
     delete pn-> Ptr();
    } else {
     ppn =&pn-> Ptr()-> pnNext;
    }
   }
  } //もう一度ロック解除を試みる
 } //関数の終わり


保留中の作業を行うには、設定されたビットSが見つかるまでリスト内を移動します。ビットSが設定されている最初の要素では、発信ポインターをリセットして残りのリストを「スチール」します。 次に、この残りをバイパスして、各イベントを設定し、アイテムを削除します。 前と同様に、リストを「盗み」、複数のスレッドからの同時呼び出しがイベントの複数のインストールにつながらないようにします。 最後に、作業が完了したら、リストのロックを解除しようとします-いつの日か新しい作業がなくなり、関数を正しく終了できることを期待して。


ご覧のとおり、「やる、書き留める(他の人に委ねる)」モデルの主なアイデアは非常に単純ですが、その実装から、すべての微妙な点を考慮して、頭が痛くなるかもしれません。 そのようなものの実装は、十分な時間、忍耐、およびこれらすべてに対処する能力があるシステムプログラマーに任せるのが最善です。 たとえば、 Arun Kishan:Inside Windows 7-Windows Kernel Dispatcher Lock別れを告げるインタビューで、Windowsカーネルに取り組んでいるアーキテクトがこの特定のモデルの使用について語っています。



All Articles