Chromium has a nice set of utilities and strong practices around enforcing thread safety both at compile time and run time. One of those is known as the ThreadChecker. When used properly, these checks bring clarity and correctness to developer builds with only a small cost in runtime overhead. This allows for catching threading bugs early in the development cycle, where they can often be reproduced, diagnosed and resolved much easier. The Chromium codebase is littered with thousands of these thread check macros, and when developing new features it’s likely you’ll hit one sooner or later.
We’ve implemented these conventions in the Meld Studio codebase and gotten a lot of value out of them. It’s also been beneficial during the code review process, as the checks help to constrain the surface area in which we might normally need to carefully review multi-threaded code.
Chromium’s implementation can be found in //base/threading/thread_checker.h. The macros come in three flavors:
THREAD_CHECKER(name)DCHECK_CALLED_ON_VALID_THREAD(name, ...)DETACH_FROM_THREAD(name, ...)In developer builds, these macros call into the base::ThreadCheckerImpl
class which will validate that the current thread matches the thread the check
was originall bound to. Otherwise, it will assert and trigger an immediate
exit with a stack trace showing where the thread was originally bound
and where the current check failed.
It is used as follows: Add a THREAD_CHECKER(thread_name_) for each thread
a class may be called from. Then in each of the methods, use
DCHECK_CALLED_ON_VALID_THREAD to both verify, but also document that
the given thread is the calling thread. DETACH_FROM_THREAD is provided
to detach the bound thread – this is needed since the THREAD_CHECKER
binds to the current thread on custruction, so we need to detach it before
it is used for the first time. More on this below.
There’s also Clang’s Thread Sanitizer which does an outstanding job of catching race conditions. On it’s own, it’s a powerful tool to detect threading issues during development or automated testing. However, it doesn’t improve the readability of the code by indicating the threading contract at the call-site. As such, Clang Thread Sanitizer should be used as a compliment to, not a replacement of ThreadChecker assertions.
In projects that aren’t able to pull in the enormous web of Chromiums
//base library, the same mechanism can be implemented (minus the stack traces)
in only 20 lines of C++20. How apt.
Here’s a very simple implementation you can copy and paste to begin using
thread checks in non-production projects. It’s missing some of the facilities
for stack traces and other quality of life improvements in Chromium’s thread
checker, but you’re free to replace assert with your own assert that has
improved diagnostics.
#include <thread>
#include <cassert>
#define THREAD_CHECKER(name) ThreadChecker name##_;
#define CHECK_CALLED_ON_VALID_THREAD(name) assert(name##_.CalledOnValidThread());
struct ThreadChecker
{
auto CalledOnValidThread() const -> bool
{
auto self = std::thread::id();
// Will re-attach if previously detached using DetachFromThread().
auto prev_value = std::thread::id();
if (thread_id_.compare_exchange_strong(prev_value, self)) {
return true;
}
return prev_value == self;
}
void DetachFromThread()
{
thread_id_.store(std::thread::id());
}
mutable std::atomic<std::thread::id> thread_id_{std::this_thread::get_id()};
};
Let’s take a Single Producer, Single Consumer queue as an example of how we might use the checker to validate all upstream callers are well-behaved.
Use the THREAD_CHECKER and CHECK_CALLED_ON_VALID_THREAD macros
as follows:
#include <span>
#include <vector>
struct SPSCQueue
{
SPSCQueue()
{
DETACH_FROM_THREAD(producer_thread_);
DETACH_FROM_THREAD(consumer_thread_);
}
void WriteData(std::span<uint8_t> data)
{
CHECK_CALLED_ON_VALID_THREAD(producer_thread_);
}
std::vector<uint8_t> ReadData(int bytes)
{
CHECK_CALLED_ON_VALID_THREAD(producer_thread_);
return std::vector<uint8_t>{};
}
THREAD_CHECKER(producer_thread_);
THREAD_CHECKER(consumer_thread_);
};
In this case, let’s assume there are three threads in play:
SPSCQueue is constructed.Since the ThreadChecker will attach to the thread id upon construction,
we need to detach in the SPSCQueue constructor body so that when the
first call comes to WriteData or ReadData, the checker will now be
“engaged” and attached to that thread until we call DetachFromThread
again.
For the price of 8 bytes of storage per checker and a bit of runtime overhead
for the thread id lookup and atomic compare-and-swap, you or any other future
travellers will know when they’ve violated a thread calling contract when they
hit an assert. Assuming your assert macro is a no-op when NDEBUG is on,
this should all compile into fairy dust in release builds.