Synchronized output streams in C++20

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 the ostringstream is empty.
  • at point [2] the string stream will contain the “Hello, World!” text since a call to emit() 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 because syncstr has not emitted its content
  • at point [2], str is still empty because neither syncstr nor syncstr2 have emitted their content
  • at point [3], str contains the text “Hello, all!” because syncstr2 has gone out of scope and therefore emitted its internal content
  • at point [4], str contains the text “Hello, all!Hello, World!” because syncstr 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.

1 Reply to “Synchronized output streams in C++20”

  1. 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.