読者です 読者をやめる 読者になる 読者になる

C++11 標準ストリームへの出力はスレッドセーフ

本記事の本題とする、「一回の書き込みにすれば排他制御は必要ない」という部分が誤りであったため、本記事を取り下げます。

C++11から標準ライブラリに、並行プログラミングのための各種機能が入った影響は、入出力のライブラリにもあります。スレッドの存在を前提とした規定が追加されています。

[iostream.objects.overview] 27.4.1 p4には、以下のようにあります:

Concurrent access to a synchronized (27.5.3.4) standard iostream object’s formatted and unformatted input (27.7.2.1) and output (27.7.3.1) functions or a standard C stream by multiple threads shall not result in a data race (1.10). [ Note: Users must still synchronize concurrent use of these objects and streams by multiple threads if they wish to avoid interleaved characters. —end note ]

同期された標準iostreamオブジェクトの書式化された・されていない入力と出力の関数、および標準Cストリームに対する複数スレッドによる並行アクセスは、データ競合を引き起こさない。[注: 交互的な文字を避けるには、これらのオブジェクトとストリームの並行使用を複数のスレッドで同期させておく必要がある。]

最初の一文での「同期」とは、並行プログラミングでの同期ではなく、標準入力と標準出力の同期のことを指します。デフォルトで同期されます。パフォーマンスのために同期を意図的に外すこともあります。

注釈のところにある「交互的な文字」とは、スレッド1がos << 1 << 2;、スレッド2がos << 3 << 4;と書き込む場合に、出力順が13241234のように不定になり、スレッド間の出力が混在することを指します。複数スレッドからの並行アクセスによってiostreamのオブジェクトがおかしな状態になったりはしませんが、複数回の書き込みでの順序保証はありません。

結果が交互的になることを避けたい場合には、os << 1 << 2;os << 3 << 4;をそれぞれ一度で書き込む必要があります。そのためには、文字列フォーマットの関数(Boost.Formatや、fmtlibなど)や、文字列ストリームなどを使用することで解決します。

void thread1()
{
    // 解決策1 : 文字列フォーマット
    os << fmt::format("{0}{1}", 1, 2);
}

void thread2()
{
    // 解決策2 : 文字列ストリーム
    std::stringstream ss;
    ss << 3 << 4;
    os << ss.str();
}

このようにすることで、出力は必ず12343412のどちらかになります。もちろん、ミューテックスを使用して一連の書き込みの順序を保証してもかまいません。

参照