Rokiのチラ裏

学生による学習のログ

Synchronized Buffered Ostream

先日、米国のニューメキシコ州アルバカーキで開催された ISO C++ 委員会による国際会議にて C++20 のドラフトに追加された Synchronized Buffered Ostream (p0053) についてのメモ。当エントリー内容は同提案書である p0053 に基づく。

C++ の一部のストリーム出力操作は競合しない事が保証されているが、その効果が実用的である事は保証されていない。C++20 から同提案によって、異なるスレッドからの出力をコヒーレントな順序で実行できるようになった。導入されるのは、std::basic_streambuf をラップした std::osyncstream (std::basic_osyncstream<char>) と std::wosyncstream (std::basic_osyncstream<wchar>) *1である。std::basic_osyncstreambasic_osyncstream::emit メンバ関数が呼び出された時、または破棄時*2に内部ストリームバッファの内容を basic_ostream のストリームバッファにアトミックに転送する。挙動は以下の通り、実際の出力やフラッシュのタイミングが少し複雑。

#include <syncstream>
#include <iostream>

{
    std::osyncstream bout{std::cout}; // std::cout の同期ラッパー
    bout << "Hello, " << "World!" << ’\n’;

    std::osyncstream{std::cout} << "The answer is " << 6*7 << std::endl; // 同じくstd::cout の同期ラッパー。フラッシュ操作だがこの時点ではフラッシュはされず、まだ出力されない。
    bout << "and more !\n";
} // 文字列がアトミックに転送され、std::cout がフラッシュされる

{
    std::osyncstream bout{std::cout};
    bout << "Hello, " << "World!" << '\n';
    bout.emit(); // 文字列が転送される。std::cout はフラッシュされない。
    bout << "and more!" << std::endl; // フラッシュ操作、しかしまだフラッシュされない。
    bout.emit(); // 文字列が転送される。std::cout がフラッシュされる。
    bout << "and...\n"; // フラッシュ操作はなし
} // 文字列が転送される、std::cout はフラッシュされない。

内部実装として、代入演算子の呼び出しにおいても emit メンバ関数が呼び出されるようだが、ラップされたストリームバッファから rhs の関連付けを解除して、rhs の破壊が出力を生成しないようにするようだ*3。 尚執筆時現在まだ取り込まれていないが、p0753r1 が追加されれば、以下のように osyncstream, wosyncstream のマニピュレータを利用してフラッシュ時の挙動を制御できるようになる。

{
    // static_cast<basic_osyncbuf<charT, traits, Allocator>*>(basic_ostream<charT, traits>().rdbuf())->set_emit_on_sync(true) が呼ばれる
    std::osyncstream{std::cout} << std::emit_on_flash << "foo" << std::endl; // 文字列が転送される。std::cout はフラッシュされる。
}
{
    // static_cast<basic_osyncbuf<charT, traits, Allocator>*>(basic_ostream<charT, traits>().rdbuf())->set_emit_on_sync(false) が呼ばれる
    std::osyncstream{std::cout} << std::noemit_on_flash << "var" << std::endl; // 文字列は転送されない。まだフラッシュされない。
} // 文字列が転送される、std::cout がフラッシュされる。
{
    // static_cast<basic_osyncbuf<charT, traits, Allocator>*>(basic_ostream<charT, traits>().rdbuf())->emit() が呼ばれる
    std::osyncstream{std::cout} << "foo" << std::flush_emit; // 文字列が転送され、フラッシュされる。
}

p0053 本文にはこのような設計となっている目的として以下のように述べられている。本文から引用。

Can you make a flush of the osyncstream mean transfer the characters and flush the underlying stream?

No, because the point of the osyncstream is to collect text into an atomic unit. Flushes are often emitted in calls where the body is not visible, and hence unintentionally break up the text. Furthermore, there may be more than one flush within the lifetime of an osyncstream, which would impose a performance loss when an atomic unit of text needs only one flush. The design decision is only to flush the underlying stream if the osyncstream was flushed, and only once per atomic transfer of the character sequence. p0053r5 introduced manipulators to allow both ways, but the default remains not to flush immediately.

Can flush just transfer the characters and not flush the underlying stream?

The flush intends an effect on visible to the user. So, we should preserve at least one flush. The flush may not be visible to the code around the osyncstream, and so its programmer cannot do a manual flush, because attempting to flush the underlying stream that is shared among threads will introduce a data race.

尚、本提案のセクション 3.8 に記載されている実装のサンプルコードは、筆者によって若干の変更が加えられた

*1:直接的なラップは std::basic_syncbuf で行われる

*2:内部実装としてはデストラクタで emit メンバ関数が呼び出される

*3: [syncstream.syncbuf.assign]