One of the many things included in C++20 is the support for synchronizing output streams for operations that may have race conditions. To understand the problem let’s begin with the following example:
int main() { std::vector<std::jthread> threads; for (int i = 1; i <= 10; ++i) { threads.push_back( std::jthread([](const int id) { std::cout << "I am thread [" << id << "]" << '\n'; }, i)); } }
We are starting a bunch of threads and what they do is printing a text to the console and then terminate. So you would expect an output like this:
I am thread [4] I am thread [3] I am thread [8] I am thread [5] I am thread [9] I am thread [6] I am thread [10] I am thread [7] I am thread [2] I am thread [1]
You cannot expect the threads to execute in the order they were started but the intention is to have an output like the one above. However, it turns out that what you get is rather scrambled text like the following:
I am thread [I am thread [4I am thread [2I am thread [7]I am thread [9] I am thread [3] I am thread [5] I am thread [10]I am thread [8] I am thread [6] ] ] 1]
The following example does not exhibit this problem. Let’s take a look:
int main() { std::vector<std::jthread> threads; auto worker = [](std::string text) { std::cout << text; }; auto names = { "Alpha", "Beta", "Gamma", "Delta", "Epsilon" }; using namespace std::string_literals; for (auto const& name : names) threads.push_back(std::jthread(worker, "Hello, "s + name + "!\n")); }
No matter how many times you run this code it always shows the output in the following form:
Hello, Alpha! Hello, Delta! Hello, Gamma! Hello, Beta! Hello, Epsilon!
In both these examples I used std::cout
to print to the output console. Obviously, there are data races that occur in the first example but not in the second. However, std::cout
is guaranteed to be thread-safe (unless sync_with_stdio(false)
has been called). The use of the operator<<
is fine, as we can see in the second example. But multiple calls to this operator<<
are not atomic and they can be interrupted and resumed after the thread resumes its execution. So if we take the line std::cout << "I am thread [" << id << "]" << '\n';
there are four calls to operator<<
. So the execution can stop between any of these and other thread will write to the output. So the output can have any of these forms:
I am thread [1]\nI am thread [2]\n
I am thread[I am thread[2]\n1]\n
I am thread[1I am thread]\n[2]\n
- etc. etc.
This means that you can solve this problem by writing to an output string stream and after having all the text that should be atomically written to the console using the std::cout
object. This is shown in the following example:
int main() { std::vector<std::jthread> threads; for (int i = 1; i <= 10; ++i) { threads.push_back( std::jthread([](const int id) { std::stringstream s; s << "I am thread [" << id << "]" << '\n'; std::cout << s.str(); }, i)); } }
In C++20, there is a simpler solution: std::basic_osyncstream
(available in the new <syncstream>
header) which provides the means for multiple threads to write to the same output stream in a synchronized way. Changes to the first example that had data races are minimal, but can have two forms:
- using a named variable
int main() { std::vector<std::jthread> threads; for (int i = 1; i <= 10; ++i) { threads.push_back( std::jthread([](const int id) { std::osyncstream scout{ std::cout }; scout << "I am thread [" << id << "]" << '\n'; }, i)); } }
- using a temporary object
int main() { std::vector<std::jthread> threads; for (int i = 1; i <= 10; ++i) { threads.push_back( std::jthread([](const int id) { std::osyncstream { std::cout } << "I am thread [" << id << "]" << '\n'; }, i)); } }
Note: There are two specializations of std::basic_osyncstream
for the common character types, std::osyncstream
for char
(that we saw in the previous snippet) and std::wosyncstream
for wchar_t
.
As long as all the writes to the same destination buffer (such as the standard output in this example) are written through instances of the std::basic_osyncstream
class, it is guaranteed that these write operations are free of data races. The way this works is that std::basic_osyncstream
wraps the output stream but also contains an internal buffer (of type std::basic_syncbuf
) that accumulates the output, where it appears as a contiguous sequence of characters. Upon destruction, or when explicitly calling the emit()
method, the content of the internal sync buffer is transferred to the wrapped stream. Let’s see some examples to understand how this works.
int main() { std::ostringstream str{ }; { std::osyncstream syncstr{ str }; syncstr << "Hello, World!"; std::cout << "[1]:" << str.str() << '\n'; } std::cout << "[2]:" << str.str() << '\n'; }
In this example, str
is a std::ostringstream
. syncstr
is a std::osyncstream
that wraps this string stream. We are writing to the synced stream. At point [1]
, calling the str()
method of ostringstream
will return an empty string, because the sync stream has not emitted the content of its internal buffer to the wrapped stream. That happens after the syncstr
object is destroyed as it goes out of scope. Therefore, at point [2]
, str
will contain the written text. The output is, therefore, as follows:
[1]: [2]:Hello, World!
We can also explicitly call emit()
to transfer the content of the internal buffer to the wrapped output stream. The following example demonstrates this:
int main() { std::ostringstream str{ }; { std::osyncstream syncstr{ str }; syncstr << "Hello, World!"; std::cout << "[1]:" << str.str() << '\n'; syncstr.emit(); std::cout << "[2]:" << str.str() << '\n'; syncstr << "Hello, all!"; std::cout << "[3]:" << str.str() << '\n'; } std::cout << "[4]:" << str.str() << '\n'; }
What happens here is that:
- at point
[1]
, nothing has been emitted, so the content of theostringstream
is empty. - at point
[2]
the string stream will contain the “Hello, World!” text since a call toemit()
previously occurred - at point
[3]
the string stream contains only “Hello, World!” even though more text has been written to the sync output stream previously - at point
[4]
the string stream contains “Hello, World!Hello, all!” since the sync output stream emitted the rest of its internal buffer upon going out of scope.
The output is as follows:
[1]: [2]:Hello, World! [3]:Hello, World! [4]:Hello, World!Hello, all!
You can also get a pointer to the wrapped stream of a std::basic_osyncstream
with a call to get_wrapped()
. This can be used to sequence content to the same stream from multiple instances of std::basic_osyncstream
. Here is an example:
int main() { std::ostringstream str{ }; { std::osyncstream syncstr{ str }; syncstr << "Hello, World!"; std::cout << "[1]:" << str.str() << '\n'; { std::osyncstream syncstr2{ syncstr.get_wrapped() }; syncstr2 << "Hello, all!"; std::cout << "[2]:" << str.str() << '\n'; } std::cout << "[3]:" << str.str() << '\n'; } std::cout << "[4]:" << str.str() << '\n'; }
In this snippet we have two std::osyncstream
objects, with different scopes, both wrapping the same string stream. What happens is that:
- at point
[1]
,str
is empty becausesyncstr
has not emitted its content - at point
[2]
,str
is still empty because neithersyncstr
norsyncstr2
have emitted their content - at point
[3]
,str
contains the text “Hello, all!” becausesyncstr2
has gone out of scope and therefore emitted its internal content - at point
[4]
,str
contains the text “Hello, all!Hello, World!” becausesyncstr
has also gone out of scope and therefore emitted its internal content
The output for this example is the following:
[1]: [2]: [3]:Hello, all! [4]:Hello, all!Hello, World!
std::osyncstream
is a C++20 standard alternative to explicitly using synchronization mechanisms (such as std::mutex
) for writing content to output streams in a data race-free manner.
Timely article for me. Just working with sync’d output for a logging class. Still wrestling with the emit() capability to flush log when a line is finished without resorting to string stream. Also seeing issue with output to std::cerr.