Because I have been writing more multithreaded C++14 code lately, I've developed some tools that help me make sure that my threads are all working as intended. In the real code, it's often difficult to detect race conditions or unintended sequence dependencies among threads, so I have found that temporarily substituting a work simulator (worksim
in the code) for the real thread code, leaving any other locks, mutexes, etc. in place, allows me to gain some insight as to how the various threads might run.
There are three main pieces in the code below. The first is the Logger
class which is more or less a mutex-protected interface to std::cout
. It takes a std::stringstream &
as input, emits the contents of it to std::cout
and the clears the input stringstream.
The second piece is the worksim
itself which employs a binomial random number distribution to generate delays that are typically around 25ms, emitting status to the passed Logger
object until the stop
flag is set. I deliberately do not want to use a conditional variable here because I want to simulate the work that the thread would do.
The third piece is a simple main
showing how these are used. It creates 10 threads, waits five seconds and then shuts everything down.
I'm interested in comments on the worksim
portion in particular. Specifically,
- is the intent clear?
- is the use of a
static
number generator OK here? - is there a better way to handle the logging?
threadplay.cpp
#include <iostream>
#include <atomic>
#include <mutex>
#include <thread>
#include <random>
#include <chrono>
#include <vector>
#include <sstream>
class Logger {
public:
Logger &operator<<(std::stringstream &ss) {
std::lock_guard<std::mutex> lock(m);
std::cout << ss.str();
ss.str(std::string{});
return *this;
}
private:
std::mutex m;
};
void worksim(Logger &mylog, std::atomic<bool> &stop, int n) {
using namespace std::chrono_literals;
static std::mt19937 gen(std::random_device{}());
std::binomial_distribution<int> dist(50, 0.5);
std::stringstream ss;
ss << "Checking " << n << "\n";
mylog << ss;
for (auto delay = dist(gen); !stop; delay = dist(gen)) {
ss << "Thread " << n << " sleeping for " << delay << "ms\n";
mylog << ss;
std::this_thread::sleep_for(1ms * delay);
}
ss << "Leaving " << n << "\n";
mylog << ss;
}
int main()
{
using namespace std::chrono_literals;
std::atomic<bool> stop{false};
std::vector<std::thread> th;
Logger mylog;
for (int i=0; i<10; ++i) {
th.push_back(std::thread(worksim, std::ref(mylog), std::ref(stop), i));
}
std::this_thread::sleep_for(5s);
stop = true;
for (std::thread &thread : th) {
thread.join();
}
}
thread_local
. That way, you don't have to guard them, but you keep the thread safety. If I'm not mistaken, it's even ok to make the distribution itselfthread_local
. \$\endgroup\$std::osyncstream
to guardstd::cout
in your logger, instead of manually locking it :) \$\endgroup\$emplace_back
your threads, it will be way more sexy :D \$\endgroup\$