C++20 による同期出力ストリーム

std::cout に同期せずに書き込むとどうなるか ?あなたは混乱します。 C++20 では、これはもうありません。

C++20 で同期出力ストリームを表示する前に、C++11 で非同期出力を表示したいと考えています。

// coutUnsynchronized.cpp

#include <chrono>
#include <iostream>
#include <thread>

class Worker{
public:
 Worker(std::string n):name(n) {};
 void operator() (){
 for (int i = 1; i <= 3; ++i) {
 // begin work
 std::this_thread::sleep_for(std::chrono::milliseconds(200)); // (3)
 // end work
 std::cout << name << ": " << "Work " << i << " done !!!" << '\n'; // (4)
 }
 }
private:
 std::string name;
};


int main() {

 std::cout << '\n';
 
 std::cout << "Boss: Let's start working.\n\n";
 
 std::thread herb= std::thread(Worker("Herb")); // (1)
 std::thread andrei= std::thread(Worker(" Andrei"));
 std::thread scott= std::thread(Worker(" Scott"));
 std::thread bjarne= std::thread(Worker(" Bjarne"));
 std::thread bart= std::thread(Worker(" Bart"));
 std::thread jenne= std::thread(Worker(" Jenne")); // (2)
 
 
 herb.join();
 andrei.join();
 scott.join();
 bjarne.join();
 bart.join();
 jenne.join();
 
 std::cout << "\n" << "Boss: Let's go home." << '\n'; // (5)
 
 std::cout << '\n';
 
}

上司には 6 人の従業員がいます (1 行目から 2 行目)。各ワーカーは、それぞれ 1/5 秒かかる 3 つの作業パッケージを処理する必要があります (3 行目)。労働者は自分の作業パッケージを終えた後、上司に向かって大声で叫びます (4 行目)。上司は、すべての従業員から通知を受け取ると、従業員を家に送ります (5 行目)。各従業員は、同僚を無視してメッセージを大声で叫びます!

  • std::cout スレッドセーフです :C++11 標準では、std::cout を保護する必要がないことが保証されています .各文字はアトミックに書き込まれます。例のような出力ステートメントがさらにインターリーブされる場合があります。このインターリーブは視覚的な問題にすぎません。プログラムは明確に定義されています。この注意事項は、すべてのグローバル ストリーム オブジェクトに有効です。グローバル ストリーム オブジェクトへの挿入と抽出 (std::cout, std::cin, std::cerr 、および std::clog ) はスレッドセーフです。より形式的に言えば: std::cout への書き込み はデータ競合に参加していませんが、競合状態を引き起こしています。これは、出力がスレッドのインターリーブに依存することを意味します。データ競合と競合状態という用語の詳細については、以前の記事「競合状態とデータ競合」をご覧ください。

どうすればこの問題を解決できますか? C++11 では、答えは簡単です。std::lock_guard などのロックを使用します。 std::cout へのアクセスを同期する . C++11 でのロックの詳細については、私の以前の投稿 Prefer Locks to Mutexes をお読みください。

// coutSynchronized.cpp

#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex coutMutex; // (1)

class Worker{
public:
 Worker(std::string n):name(n) {};
 
 void operator() (){
 for (int i = 1; i <= 3; ++i) { 
 // begin work
 std::this_thread::sleep_for(std::chrono::milliseconds(200));
 // end work
 std::lock_guard<std::mutex> coutLock(coutMutex); // (2)
 std::cout << name << ": " << "Work " << i << " done !!!" << '\n';
 } // (3)
 }
private:
 std::string name;
};


int main() {

 std::cout << '\n';
 
 std::cout << "Boss: Let's start working." << "\n\n";
 
 std::thread herb= std::thread(Worker("Herb"));
 std::thread andrei= std::thread(Worker(" Andrei"));
 std::thread scott= std::thread(Worker(" Scott"));
 std::thread bjarne= std::thread(Worker(" Bjarne"));
 std::thread bart= std::thread(Worker(" Bart"));
 std::thread jenne= std::thread(Worker(" Jenne"));
 
 herb.join();
 andrei.join();
 scott.join();
 bjarne.join();
 bart.join();
 jenne.join();
 
 std::cout << "\n" << "Boss: Let's go home." << '\n';
 
 std::cout << '\n';

}

coutMutex 行 (1) で、共有オブジェクトを保護します std::cout . coutMutex を入れる std::lock_guardcoutMutex であることを保証します std::lock_guard. のコンストラクタ (2 行目) でロックされ、デストラクタ (3 行目) でロック解除されます coutMutex のおかげで coutLock によって守られています 混乱は調和になります。

C++20 では、書き込みは std::cout に同期されます 簡単です。 std::basic_sync buf は std::basic_streambuf のラッパーです .出力をバッファに蓄積します。ラッパーは、破棄されると、そのコンテンツをラップされたバッファーに設定します。その結果、コンテンツは連続した一連の文字として表示され、文字のインターリーブは発生しません。
std::basic_osyncstream のおかげで 、 std::cout に直接同期的に書き込むことができます 名前付き同期出力ストリームを使用する.
前のプログラム coutUnsynchronized.cpp は次のようになります。 std::cout に同期して書き込むようにリファクタリングされています .これまでのところ、GCC 11 のみが同期出力ストリームをサポートしています。

// synchronizedOutput.cpp

#include <chrono>
#include <iostream>
#include <syncstream>
#include <thread>

class Worker{
public:
 Worker(std::string n): name(n) {};
 void operator() (){
 for (int i = 1; i <= 3; ++i) {
 // begin work
 std::this_thread::sleep_for(std::chrono::milliseconds(200));
 // end work
 std::osyncstream syncStream(std::cout); // (1)
 syncStream << name << ": " << "Work " << i // (3)
<< " done !!!" << '\n'; } // (2) } private: std::string name; }; int main() { std::cout << '\n'; std::cout << "Boss: Let's start working.\n\n"; std::thread herb= std::thread(Worker("Herb")); std::thread andrei= std::thread(Worker(" Andrei")); std::thread scott= std::thread(Worker(" Scott")); std::thread bjarne= std::thread(Worker(" Bjarne")); std::thread bart= std::thread(Worker(" Bart")); std::thread jenne= std::thread(Worker(" Jenne")); herb.join(); andrei.join(); scott.join(); bjarne.join(); bart.join(); jenne.join(); std::cout << "\n" << "Boss: Let's go home." << '\n'; std::cout << '\n'; }

以前のプログラム coutUnsynchronized.cpp からの唯一の変更点 それは std::cout です std::osyncstream でラップされています (ライン1)。 std::osyncstream の場合 行 (2) で範囲外になり、文字が転送されて std::cout フラッシュされます。 std::cout メイン プログラムでの呼び出しはデータ競合を引き起こさないため、同期する必要はありません。出力は、スレッドの出力の前または後に発生します。


syncStream を使っているので 行 (3) で一度だけ宣言されている場合は、一時オブジェクトの方が適切な場合があります。次のコード スニペットは、変更された呼び出し演算子を示しています:

void operator()() {
 for (int i = 1; i <= 3; ++i) { 
 // begin work
 std::this_thread::sleep_for(std::chrono::milliseconds(200));
 // end work
 std::osyncstream(std::cout) << name << ": " << "Work " << i << " done !!!" 
 << '\n';
 }
}

std::basic_osyncstream syncStream は 2 つの興味深いメンバー関数を提供します。

    • syncStream.emit() バッファリングされたすべての出力を発行し、保留中のすべてのフラッシュを実行します。
    • syncStream.get_wrapped() ラップされたバッファへのポインタを返します。

cppreference.com は、get_wrapped を使用して異なる出力ストリームの出力を順序付ける方法を示しています。 メンバー関数。

// sequenceOutput.cpp

#include <syncstream>
#include <iostream>
int main() {
 
 std::osyncstream bout1(std::cout);
 bout1 << "Hello, ";
 {
 std::osyncstream(bout1.get_wrapped()) << "Goodbye, " << "Planet!" << '\n';
 } // emits the contents of the temporary buffer
 
 bout1 << "World!" << '\n';
 
} // emits the contents of bout1

次は?

わお!これで C++20 は終わりです。私は C++20 について約 70 の記事を書きました。 C++20 の詳細については、私の著書「C++20:Get the Details」をご覧ください。

しかし、まだ 1 つの機能があります。コルーチンについてさらに詳しく説明したいと思います。次の投稿では、新しいキーワード co_return を試してみます。 、 co_yield 、および co_await.