1. 導入
マルチスレッドプログラミングにおいて、複数のスレッドの処理を特定の地点で同期させる「待ち合わせ」は頻繁に発生します。これまで、私たちは `std::condition_variable` を用いてこれを実装してきましたが、コードが複雑になりやすく、ミューテックスのオーバーヘッドが性能ボトルネックになるという課題がありました。C++20で導入された `std::latch` と `std::barrier` は、これらの課題を解決し、より直感的かつ高パフォーマンスな同期を実現します。
2. 基礎知識
`std::latch` と `std::barrier` は、スレッド間の同期を管理する新しいプリミティブです。
std::latch は、カウンタが0になるまで待機する「使い捨て」の同期オブジェクトです。初期化時に指定した回数だけ `count_down` が呼ばれるまで、呼び出しスレッドは待機します。
std::barrier は、指定した数のスレッドが到達するまで待機する「再利用可能」な同期オブジェクトです。各フェーズの終了時に指定した関数(完了関数)を実行できるのが特徴で、反復的な並列処理に適しています。
3. 実装/解決策
これらのプリミティブは、内部的にアトミック操作を活用して実装されています。従来の `std::condition_variable` がミューテックスのロックを介してスレッドを管理するのに対し、`std::latch` などはOSの低レベルな待機機構(LinuxのFutexなど)を直接活用することで、ロックフリーに近い効率的な待機を実現します。これにより、スレッドのコンテキストスイッチのオーバーヘッドを最小限に抑えることが可能です。
4. サンプルプログラム
以下のコードでは、4つのタスクを並列実行し、すべての完了を `std::latch` で待機する例を示します。
include
include
include
int main() {
// 4つのタスクを待つためのlatchを生成
std::latch work_done(4);
for (int i = 0; i < 4; ++i) { std::thread([&work_done, i]() { // ここで重い処理を行うと仮定 std::cout << "タスク " << i << " 完了" << std::endl; // カウンタを減らし、待機中のメインスレッドに通知 work_done.count_down(); }).detach(); } // すべてのスレッドがcount_downを呼ぶまでここで待機する work_done.wait(); std::cout << "すべてのタスクが終了しました。" << std::endl; return 0; }
5. 応用・注意点
実務での利用にあたっては、以下の点に注意してください。
std::latchの使い捨て性:`std::latch` は一度0になるとリセットできません。繰り返しの同期が必要な場合は、必ず `std::barrier` を選択してください。
完了関数のオーバーヘッド:`std::barrier` の完了関数は、フェーズが完了した瞬間に、最後に到達したスレッドによって実行されます。この関数内で重い処理を行うと、他のスレッドの待機時間が長引くため、可能な限り軽量な処理に留めるのがベストプラクティスです。
パフォーマンスの罠:`std::condition_variable` に比べてオーバーヘッドは小さいですが、スレッド数が極端に多い場合や、同期頻度が非常に高い場合は、キャッシュラインの競合(False Sharing)が発生する可能性があります。適切な粒度で並列化を行う設計が重要です。

コメント