#concurrency
12 posts tagged #concurrency.
What C++23 and C++26 promise about a moved-from function wrapper
None of the standard function wrappers promises an empty source after a move: std::function, move_only_function and copyable_function all leave it valid but unspecified. libstdc++ 16.1 empties all three, libc++ trunk keeps a copy in std::function and does not ship the other two. A small wrapper that states its own move works on both.
Hand a task to another thread without leaving a copy behind
Two ways to stop a std::function from leaving a copy behind when it crosses to another thread: empty the source with an exchange, or box the task in a shared_ptr at the entry point. In a demo on libc++, the plain by-value chain destroys the task on the sending thread and both fixes destroy it on the worker.
A moved-from std::function can still hold the callable
The standard leaves a moved-from std::function valid but unspecified. libc++ keeps a copy of a small callable in the source, libstdc++ empties it. A Qt HarmonyOS bug filed on 2026-10-12 shows the cost: a task queued to another thread was destroyed on the thread that sent it.
The fastest loggers do not format on the calling thread
Every fix in this series synchronizes on the calling thread. The fastest loggers do not: callers enqueue a raw record and a background thread formats and writes it. A mini producer-consumer logger, the spdlog/Quill/NanoLog numbers (around 250 ns versus 7 to 11 ns), and the crash-survivability trade-off. Episode 9 of the concurrent I/O series.
Thread-safe is not reentrant, and the difference bites
The Qt handler contract asked for a reentrant function, not just a thread-safe one, and a std::mutex gives you the second without the first. A demo shows a thread-safe counter and a reentrant recursion that needs a recursive_mutex. The precise taxonomy: reentrant implies thread-safe, never the reverse. Episode 8 of the concurrent I/O series.
Ship the C++20 feature and its fallback in one file
The code review that started this series ended with a portability question: use std::osyncstream where it exists, fall back to a mutex where it does not. The feature-test macro __cpp_lib_syncbuf and <version> are the tool for exactly that. One file, the best available tool on each compiler. Episode 6 of the concurrent I/O series.
C++26 rewrote std::print's internals and backported the fix
std::print's per-call atomicity extends to your own types via std::formatter. How it stays both safe and efficient is a C++26 story: P3107 adds locking-aware entry points and a formatter opt-in to avoid deadlock, and ships as a backport into C++23. Episode 5 of the concurrent I/O series.
std::print does not tear the way std::cout does
The fixes so far needed cooperation or a lock. C++23's std::print gives per-call atomicity for free: a single print call never interleaves with another, the way printf never did and std::cout always could. But the guarantee is per-call, so a line split across two calls can still tear. Episode 4 of the concurrent I/O series.
static std::mutex is safe to construct, thanks to magic statics
Before osyncstream, the fix for a shared log sink was a static std::mutex. But is the static mutex itself safe to initialize under threads? Yes, thanks to C++11 magic statics: a function-local static is constructed exactly once even under a stampede. With the mutex-versus-osyncstream trade-off. Episode 3 of the concurrent I/O series.
std::osyncstream makes concurrent output atomic
std::cout garbled its own output across threads. C++20's std::osyncstream fixes it: each thread buffers a line privately and emits it to the stream atomically on destruction. One wrapper, whole lines, with one sharp edge: the guarantee holds only if every writer uses it. Episode 2 of the concurrent I/O series.
Your std::cout logging has no data race and still tears
Qt's message-handler contract says the handler must be reentrant: called from many threads at once. Point it at std::cout and the output tears, even though the C++ standard guarantees no data race. 'No data race' and 'no interleaving' are different promises, and this series closes the gap. Episode 1 of the concurrent I/O series.
The syscall behind C++ asymmetric fences
Asymmetric fences let you pay for a memory barrier only on the rare path. The common path gets a free compiler barrier; the uncommon path calls Linux membarrier(), which forces every other thread to run a full fence for you. Ryan Chung Yi Sheng's deep-dive follows the idea from the C++ standard down to the kernel, and finds a possible crack in the wording along the way.