std::osyncstream
C++20 synchronized output stream that atomically transfers buffered content to a wrapped ostream, eliminating interleaved output in multithreaded programs.
std::osyncstreamsince C++20A stream wrapper that buffers all output locally and atomically transfers it to an underlying std::ostream upon destruction or an explicit emit() call, guaranteeing no interleaving with output from other osyncstream instances wrapping the same stream.
Overview
Before C++20, writing to shared output streams like std::cout from multiple threads produced unpredictable, interleaved output. The only remedy was an external std::mutex guarding every write site β fragile and easy to forget. C++20 introduced <syncstream> to address this at the stream level.
The facility consists of two cooperating components:
std::basic_syncbuf<CharT, Traits, Allocator>β astd::basic_streambufthat accumulates characters in an internal buffer and, when told to emit, acquires a per-wrapped-stream lock and atomically transfers the entire buffer to the wrapped streambuf.std::basic_osyncstream<CharT, Traits, Allocator>β astd::basic_ostreamthat owns abasic_syncbuf, giving it the familiar<<interface. The standard provides two ready-made specialisations:
using osyncstream = basic_osyncstream<char>; // C++20
using wosyncstream = basic_osyncstream<wchar_t>; // C++20The atomicity guarantee is at the transfer level: one emit() call transfers the complete buffer in one indivisible step relative to other osyncstream instances targeting the same underlying stream. The ordering between concurrent emissions is unspecified, but output units are never split or interleaved.
Syntax
#include <syncstream> // C++20
// Construction
std::osyncstream(std::ostream& wrapped);
std::osyncstream(std::ostream& wrapped, const Allocator& a);
std::osyncstream(std::osyncstream&&) noexcept;
// Key members
void emit(); // atomically transfer buffer now; clears buffer
syncbuf_type* rdbuf() const; // pointer to the internal basic_syncbuf
std::streambuf* get_wrapped() const; // wrapped stream's streambuf, or nullptr if moved-fromosyncstream is not copyable. Move construction transfers ownership of the syncbuf (and any buffered content); the moved-from object is left with no wrapped stream and any subsequent writes are silently discarded.
Examples
Thread-safe logging without a mutex
#include <syncstream>
#include <iostream>
#include <thread>
#include <vector>
#include <format> // C++20
void worker(int id, int iterations) {
for (int i = 0; i < iterations; ++i) {
// Each osyncstream scope is one atomic output unit. // C++20
std::osyncstream out(std::cout);
out << std::format("[thread {:02d}] iteration {}\n", id, i);
// out's destructor calls emit() β buffer transferred atomically here.
}
}
int main() {
constexpr int kThreads = 8;
std::vector<std::thread> threads;
threads.reserve(kThreads);
for (int i = 0; i < kThreads; ++i)
threads.emplace_back(worker, i, 100);
for (auto& t : threads)
t.join();
}Without osyncstream, std::cout writes from different threads interleave character-by-character (or at best, call-by-call). Here every complete line from a single iteration is guaranteed to appear contiguously in the output.
Controlling the emission boundary
The scope of the osyncstream object defines what counts as one atomic unit. Multiple << calls on the same object accumulate into a single emission:
#include <syncstream>
#include <iostream>
#include <chrono>
void report(int task_id, double elapsed_ms) {
std::osyncstream out(std::cout); // C++20
out << "task=" << task_id
<< " elapsed=" << elapsed_ms << "ms"
<< " status=ok\n";
// All four << calls land together β no other thread can inject between them.
}If you need to emit mid-function and then continue writing a second atomic unit:
void two_phase_log(std::ostream& sink, int id) {
std::osyncstream out(sink); // C++20
out << "[" << id << "] phase 1 starting\n";
out.emit(); // atomic transfer β buffer is now clear
// ... do work ...
out << "[" << id << "] phase 2 complete\n";
// destructor emits the second chunk atomically
}emit() clears the internal buffer and resets the stream's error state flags relative to the syncbuf. The osyncstream remains usable after emit().
Wide-character output
#include <syncstream>
#include <iostream>
#include <thread>
void wide_worker(int id) {
std::wosyncstream wout(std::wcout); // C++20
wout << L"Thread " << id << L" reporting\n";
}std::wosyncstream targets std::wostream and wraps std::basic_syncbuf<wchar_t>. All synchronization semantics are identical to the char specialisation.
Wrapping a file stream
osyncstream works with any std::ostream, not just std::cout:
#include <syncstream>
#include <fstream>
#include <thread>
#include <vector>
int main() {
std::ofstream log_file("app.log");
auto worker = [&](int id) {
std::osyncstream out(log_file); // C++20
out << "worker " << id << " done\n";
};
std::vector<std::thread> ts;
for (int i = 0; i < 16; ++i)
ts.emplace_back(worker, i);
for (auto& t : ts)
t.join();
}The underlying log_file must outlive all osyncstream objects that wrap it β the osyncstream holds a non-owning reference to the wrapped stream's streambuf.
Best Practices
One osyncstream per logical output unit. The object's lifetime defines the atomic unit. If you create one osyncstream per function and use it for several logically unrelated writes, those writes are merged into one emission β which may be what you want, or may be an accident. Be intentional.
Never share an osyncstream across threads. osyncstream itself is not thread-safe. Each thread should construct its own instance wrapping the same underlying ostream. The synchronisation happens inside syncbuf::emit(), not at the osyncstream level.
No external mutex needed when all writers use osyncstream. The internal per-stream lock inside basic_syncbuf is sufficient. Adding an external mutex is redundant and may cause deadlock if emit() also tries to acquire it.
Prefer scope-based emission. Relying on the destructor is simpler and exception-safe. Reserve explicit emit() for cases where you genuinely need two separate atomic output units from a single function.
Common Pitfalls
Mixing direct writes with osyncstream writes. Writing directly to std::cout (bypassing osyncstream) while other threads use osyncstream on std::cout is well-defined but ordering is unspecified. Direct writes are not subject to syncbuf's lock, so they may appear between β or even within β an osyncstream emission on some implementations.
Atomicity is at the streambuf boundary, not the OS boundary. emit() atomically transfers to the wrapped streambuf's internal buffer. Whether that buffer's subsequent write to the OS is atomic is the underlying stream's concern, not syncbuf's. In practice this is irrelevant for most use cases, but matters if you are mixing osyncstream with POSIX write() calls on the same file descriptor.
Moved-from objects silently discard writes. After a move, get_wrapped() returns nullptr. Any << operations on the moved-from object succeed without error but produce no output. This is easy to trigger accidentally when storing osyncstream in a container and later moving from it.
Error propagation is deferred. If emit() fails (e.g., the underlying stream has badbit set), failbit is set on the osyncstream. Errors from the underlying stream are not visible until after the transfer. Check out.good() after emit() or after the destructor runs (via a wrapper) if correctness depends on the write succeeding.
See Also
std::basic_syncbufβ the underlying buffer; accessible viaosyncstream::rdbuf(), useful when you need to pass syncbuf-level control to code that accepts astreambuf*std::mutex/std::lock_guardβ the pre-C++20 approach to guarding shared stream accessstd::atomicβ orthogonal synchronisation primitive for data rather than stream output