C++20 のセマフォ

セマフォは、共有リソースへの同時アクセスを制御するために使用される同期メカニズムです。また、卓球をすることもできます。

カウンティング セマフォは、ゼロより大きいカウンタを持つ特別なセマフォです。カウンターはコンストラクターで初期化されます。セマフォを取得するとカウンタが減少し、セマフォを解放するとカウンタが増加します。カウンターがゼロのときにスレッドがセマフォを取得しようとすると、別のスレッドがセマフォを解放してカウンターをインクリメントするまでスレッドはブロックされます。

Edsger W. Dijkstra 発明セマフォ


オランダのコンピュータ科学者 Edsger W. Dijkstra は、1965 年にセマフォの概念を発表しました。セマフォは、キューとカウンターを持つデータ構造です。カウンタはゼロ以上の値に初期化されます。 2 つの操作 wait をサポートします と signal . wait セマフォを取得し、カウンターを減らします。カウンターがゼロの場合、セマフォを取得するスレッドをブロックします。 signal セマフォを解放し、カウンターを増やします。枯渇を避けるために、ブロックされたスレッドがキューに追加されます。

本来、セマフォは鉄道の信号機です。

元のアップロード者は、英語版ウィキペディアの AmosWolfe でした。 - en.wikipedia から Commons. に移管、CC BY 2.0

C++20 でのセマフォのカウント

C++20 は std::binary_semaphore をサポートします 、これは std::counting_semaphore<1> のエイリアスです .この場合、最小最大値は 1 です。 std::binary_semaphores ロックの実装に使用できます。

using binary_semaphore = std::counting_semaphore<1>;


std::mutex とは対照的に 、 std::counting_semaphore スレッドにバインドされていません。これは、セマフォの取得と解放の呼び出しが異なるスレッドで発生する可能性があることを意味します。次の表は、std::counting_semaphore のインターフェースを示しています。 .

コンストラクター呼び出し std::counting_semaphore<10> sem(5) 少なくとも最大値 10 とカウンター 5 を持つセマフォ sem を作成します。呼び出し sem.max() 最小の最大値を返します。 sem.try_aquire_for(relTime) 相対的な期間が必要です。メンバ関数 sem.try_acquire_until(absTime) 絶対的な時点が必要です。時間の長さと時間ポイントについては、以前の記事の時間ライブラリー:time で詳しく読むことができます。 3 つの呼び出し sem.try_acquire, sem.try_acquire_for 、および sem.try_acquire_until 呼び出しの成功を示すブール値を返します。

セマフォは通常、送信側と受信側のワークフローで使用されます。たとえば、セマフォ sem を 0 で初期化すると、レシーバ sem.acquire() がブロックされます 送信者が sem.release() を呼び出すまで呼び出します .したがって、受信者は送信者の通知を待ちます。スレッドの 1 回限りの同期は、セマフォを使用して簡単に実装できます。

// threadSynchronizationSemaphore.cpp

#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>

std::vector<int> myVec{};

std::counting_semaphore<1> prepareSignal(0); // (1)

void prepareWork() {

 myVec.insert(myVec.end(), {0, 1, 0, 3});
 std::cout << "Sender: Data prepared." << '\n';
 prepareSignal.release(); // (2)
}

void completeWork() {

 std::cout << "Waiter: Waiting for data." << '\n';
 prepareSignal.acquire(); // (3)
 myVec[2] = 2;
 std::cout << "Waiter: Complete the work." << '\n';
 for (auto i: myVec) std::cout << i << " ";
 std::cout << '\n';
 
}

int main() {

 std::cout << '\n';

 std::thread t1(prepareWork);
 std::thread t2(completeWork);

 t1.join();
 t2.join();

 std::cout << '\n';
 
}

std::counting_semaphore prepareSignal (1) は 0 または 1 の値を持つことができます。具体的な例では、0 で初期化されます (1 行目)。これは、呼び出し prepareSignal.release() を意味します 値を 1 に設定し (2 行目)、呼び出しのブロックを解除します prepareSignal.acquire() (3行目).

セマフォでピンポンをして、小さなパフォーマンス テストを行いましょう。

卓球ゲーム

前回の投稿「C++20 における条件変数とアトミックのパフォーマンス比較」では、ピンポン ゲームを実装しました。ゲームのアイデアは次のとおりです。1 つのスレッドが ping を実行します。 関数と他のスレッド pong 関数。 ping スレッドは、pong スレッドの通知を待ち、その通知を pong スレッドに送り返します。 1,000,000 ボールの変更後にゲームが停止します。各ゲームを 5 回実行して、同等のパフォーマンス数値を取得します。ゲームを始めましょう:

// pingPongSemaphore.cpp

#include <iostream>
#include <semaphore>
#include <thread>

std::counting_semaphore<1> signal2Ping(0); // (1)
std::counting_semaphore<1> signal2Pong(0); // (2)

std::atomic<int> counter{};
constexpr int countlimit = 1'000'000;

void ping() {
 while(counter <= countlimit) {
 signal2Ping.acquire(); // (5)
 ++counter;
 signal2Pong.release();
 }
}

void pong() {
 while(counter < countlimit) {
 signal2Pong.acquire();
 signal2Ping.release(); // (3)
 }
}

int main() {

 auto start = std::chrono::system_clock::now();

 signal2Ping.release(); // (4)
 std::thread t1(ping);
 std::thread t2(pong);

 t1.join();
 t2.join();

 std::chrono::duration<double> dur = std::chrono::system_clock::now() - start;
 std::cout << "Duration: " << dur.count() << " seconds" << '\n';

}

プログラム pingPongsemaphore.cpp 2 つのセマフォを使用します:signal2Pingsignal2Pong (1 と 2)。どちらも 0 と 1 の 2 つの値を持つことができ、0 で初期化されます。これは、セマフォ signal2Ping, の値が 0 の場合を意味します。 電話 signal2Ping.release() (3 と 4) は値を 1 に設定するため、通知になります。 signal2Ping.acquire() (5) 値が 1 になるまで呼び出しをブロックします。同じ引数が 2 番目の semaphore signal2Pong にも当てはまります。 .

平均して、実行時間は 0.33 秒です。

すべての卓球ゲームのパフォーマンス数値をまとめてみましょう。これには、前回の投稿「C++20 における条件変数とアトミックのパフォーマンス比較」と、セマフォで実装されたこのピンポン ゲームのパフォーマンス数値が含まれます。

すべての番号

条件変数は最も遅い方法であり、アトミック フラグはスレッドを同期する最も速い方法です。 std::atomic の性能 間にあります。 std::atomic には欠点が 1 つあります。 . std::atomic_flag 常にロックフリーである唯一のアトミック データ型です。セマフォはアトミック フラグとほぼ同じ速度であるため、最も感銘を受けました。

次は?

ラッチとバリアにより、C++20 にはより便利な調整型があります。次回の投稿で紹介させてください。