Let’s start by introducing a fun program which has a slight defect (can you spot it?):
#include <future>
#include <vector>
#include <iostream>
using namespace std;
void print(int n) {
cout << "This is line " << n << '\n';
}
int main() {
vector<future<void>> tasks;
for (int i{}; i != 10; ++i) {
tasks.push_back(async(launch::async, &print, i + 1));
}
}
When run (under Windows 10) it produced the following output for a sample run:
This is line 1This is line 2
This is line
This is line 43
This is line 5
This is line This is line 6
This is line 87
This is line 9
This is line
10
The problem is that while the global streams are thread-safe in Modern C++, accessing them from different threads can cause interleaved output. One way of solving this is to create (custom) manipulators which operate on a global std::mutex
, locking it at the beginning of a stream output sequence and unlocking it at the end. Writing these new manipulators on the same line, we get:
cout << lock << "This is line " << n << '\n' << unlock;
To make this work, we need two functions which take and return a reference to a std::ios_base
. (This is how the parameterless manipulators of the Standard Library are defined.) Here is a program which shows this in action:
#include <mutex>
#include <future>
#include <ios>
#include <vector>
#include <iostream>
using namespace std;
mutex ostream_mutex{};
ios_base& lock(ios_base& strm) {
ostream_mutex.lock();
return strm;
}
ios_base& unlock(ios_base& strm) {
ostream_mutex.unlock();
return strm;
}
void print(int n) { // note: using lock and unlock
cout << lock << "This is line " << n << '\n' << unlock;
}
int main() {
cout << nounitbuf; // note: turn off buffering
vector<future<void>> tasks;
for (int i{}; i != 10; ++i) {
tasks.push_back(async(launch::async, &print, i + 1));
}
}
This corrected program produced the following output for a sample run; note that while the relative order is not guaranteed, there is no “messy” interleaved output:
This is line 1
This is line 2
This is line 4
This is line 3
This is line 6
This is line 5
This is line 8
This is line 7
This is line 10
This is line 9
These manipulators can also be used with std::cerr
and std::clog
to prevent messy output on the console when combining output from more than one stream, however it may be advisable both to turn off buffering for std::cout
(with a call to std::cout.setf(std::ios::unitbuf);
) and to “untie” it from std::cerr
(with std::cerr.tie(nullptr);
).
There are other ways of solving the same problem, usually using a std::lock_guard<std::mutex>
, but this requires creating a sub-scope for every single std::cout
etc. call in your program. (The need for a global mutex is also still present.) I feel the implementation of the lock/unlock functionality inside custom manipulators is neat, and solves the problem efficiently and in good C++ style.