楽観的な同期プリミティブ、キュー、およびすべてすべて。 3つの行為における悲喜劇

私は事前に警告します、主題にいる人々のために、非常に多くの興味深いものはありません。 :)



利用可能な唯一のプリミティブである同期キューのみを使用して、基本的な同期プリミティブ( mutexセマフォ 、および読み取り/書き込みロック )を実装する緊急のタスクがありました。 同時に、途中でスピンロックがどのように配置されるかを説明し、少しフランケンシュタインを収集します。



パート1:すべて-キュー

同期キューは次のように機能します。queue.get()はキュー(番号)から最初のイベントを返します。そうでない場合は、ストリームを安楽死させます。 queue.post(数値)はイベントを送信し、get()でイベントを待機しているスレッドの1つを起動します。



1.1ミューテックス

したがって、初心者にとって最も簡単なタスクはミューテックスです。 名前が示すように、それは(mutex)リソースへの排他的アクセスを提供します。 この場合のリソースは、キュー内の単一のイベントです。 それは:



初期化:

queue.post(0);

キャプチャ:

queue.get(); //キューが空の場合はスリープします。

免除:

queue.post(0); //イベントをその場所に返します。



1.2セマフォ

単純に、このスキームはセマフォに拡張できます-初期化でキューに何も入れずに、セマフォカウンタをqueue.post(0)だけ増やし、queue.get()を減らします。



1.3 RWLock

現在、最も難しい部分は読み取り書き込みロックです。 最も簡単なソリューションは、セマフォとミューテックスから構築できます。 このオプションの欠点は、最初に同時読み取りスレッドの最大数を記録する必要があることです。 これは技術的にどのように行われますか? 非常にシンプル:



初期化:

クライアントの最大数でセマフォを初期化します。

読み取りキャプチャ:

セマフォを取得します(クライアントの数が1つ減ります)

読み取り免除:セマフォを解放します(クライアントの数が1つ増えます)

録画キャプチャ:

ミューテックスを取得し、セマフォで最大数のクライアントを取得します(すべての読み取りストリームがロックを解放するまで待機します)。書き込みストリーム間でレコードのロックをシリアル化するには、ミューテックスが必要です。 デッドロック。 セマフォを完全にキャプチャした後、ミューテックスを解放します。 (セマフォ0のカウンタ)

免除:

ミューテックスを取得し、セマフォを解放します(カウンターは再び読み取ったクライアントの最大数に等しくなり、ミューテックスを解放します)。



2段階で、同様のスキームを実装できますが、クライアントの最大数を指定しません。

読み取りキュー(read_queue)と書き込みキュー(write_queue)の2つのキューを作成します。 両方のキューでゼロを送信します。 レコードのキャプチャは非常に単純です(最初のミューテックスのように)。キャプチャする代わりに、write_queue.get()を実行し、write_queue.post(0)でリリースします。 読み取りキャプチャはもう少し複雑に見えます。最初にキューから番号を読み取り、turn = read_queue.get()、次に-ターン== 0の場合、書き込みも書き込み-write_queue.get()をキャプチャします。 これが失敗した場合、それは何もせず、次の読み取りスレッドはread_queue.get()でスリープ状態になり、フライトはありません。 read_queueに送信します(turn + 1)。 read_queue.post(turn + 1)。 書き込みロックは戻りません。 そのようなロックを解除するときは、次の手順を実行します。turn = read_event.get()-1; turn == 0の場合、書き込みロックを所定の位置に戻す必要があります。 その後、read_event.post(turn)を実行します。



パート2:スピンロック

スピンロックは、プロセス空間でほとんどの同期作業を行うための優れた方法です。 これは、プロセッサやプラットフォームがハードウェア同期プリミティブをサポートしている場合に機能します。 通常、このような命令のセマンティクスは、フェッチアンドアド(FAA)、コンペアアンドスワップ(CAS)の複数のグループに分類されます。 他にもありますが、それらはよりエキゾチックであり、私はそれらに触れません。



もちろん、最も興味深いのはFAAとCASです。 Fetch-And-Add&mdashは、メモリ内の変数の値を任意の数だけアトミックにインクリメントし、前の値を返すようなプロセッサ命令です。 CAS A、B、Cは、メモリ[A]の値と目的のBの値をアトミックに比較し、すべてが正しい場合、メモリの値をCに設定します。x86プロセッサでは、CASはcmpxchg8b命令で表されます。



この場合のスピンロックは非常にシンプルで高速なものです。 オペレーティングシステムを呼び出して貴重なプロセッササイクルを無駄にする代わりに、同期を自分で調整できます。 最も単純なスピンロックは、システムに時間を戻すことさえしません。目的の数がメモリに表示されるまで、単純にループでスピンできます。 この方法にはさまざまな改善点がありますが、この入門記事の範囲を超えています。 :)



2.1ミューテックス

次に、最も単純な例で、これらのプリミティブでのmutexの実装を示します。

CASによる実装:

初期化:

値= 1

キャプチャ:

while(__ compare_and_swap(&value、1、0)!= 1){/ *メモリセルに1が含まれるまで待機します。これは、リソースが解放されることを意味します(1の場合、アトミックに0 * /}を書き込みます)

免除:

値= 1; / *メモリ位置に1を書き込むだけ* /



FAAではもう少し難しい:

チケット(チケット)と優先度(ターン)の2つの番号を入力する必要があります。最初は両方とも0が含まれています。

ミューテックスキャプチャ:

my_turn = __fetch_and_add(&ticket、1); //移動数を取得します。

while(turn!= my_turn){/ *移動を待っている* /}

免除:

__fetch_and_add(&turn、1); //移動を増やすと、次のクライアントがロックを受け取ります。



残りのプリミティブ、好奇心itive盛な読者は自分で思い付くことができ、ノーベル賞を獲得することさえできます!



パート3:疲れた、眠りたい、いつ終わるのか? 楽観。

次に、セマフォフランケンシュタインを収集します。 考えてみると、キューにある実際のメッセージの形で無料のリソースを提示し、これに貴重なリソースを費やす必要はありません。 取得と追加が役立ちます。

セマフォのキャプチャ(待機):

old_value = __fetch_and_add(&value、-1);

ここでold_valueが0より大きい場合、人生を楽しんでおり、オペレーティングシステムの機能を呼び出さず、十分なリソースがあり、すべてが正常です。



セマフォ (post)を解放すると 、カウンターが1増加します。old_value = __fetch_and_add(&value、1);。

ここでold_valueがゼロ未満の場合、誰かがセマフォでスリープしていたので、event.post(0)を起動する必要があります;。



出来上がり! オペレーティングシステムを使用しない楽観的に 、非常に軽量でシンプルなセマフォを得ました。 たとえば、ミューテックスは値1のセマフォから取得され、rw-lockはセマフォから取得され、ミューテックスは1.3節から取得されます。



最後まで読んでくれてありがとう! :)




All Articles