Improving C++ thread safety with thread check assertions

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:

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()};
};

Example Usage

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:

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.