Notes on Folly: Thread Name

2019-09-22
c++

On Linux, we can give each thread a name so debugging multi-thread program is easier. pthread_setname_np is the call to name a thread. It's as easy as that. In this post, however, I want to write more details about the implementation.

In folly/system/ThreadName.h, we have the following functions for setting thread names:

bool canSetCurrentThreadName();

bool canSetOtherThreadName();

bool setThreadName(std::thread::id tid, StringPiece name);

bool setThreadName(pthread_t pid, StringPiece name);

bool setThreadName(StringPiece name);

Let’s run something really simple

#include <folly/init/Init.h>
#include <folly/system/ThreadName.h>
#include <glog/logging.h>

int main(int argc, char **argv) {
  folly::init(&argc, &argv);

  auto thr = std::thread([]() {
    folly::setThreadName("newThread");
    LOG(INFO) << "new thread exits";
  });
  auto name = folly::getThreadName(thr.get_id());
  LOG(INFO) << "Main waiting on "
            << (name.hasValue() ? name.value() : " the other thread");
  thr.join();
}

The main thread pulls thr‘s name, which is racy. When the setThreadName is not called, what is the default thread name?

~ $ ./a.out -logtostderr
I0922 16:50:37.111975   957 thread.cc:11] new thread exits
I0922 16:50:37.111953   956 thread.cc:14] Main waiting on a.out
~ $ ./a.out -logtostderr
I0922 16:50:37.515489   958 thread.cc:14] Main waiting on newThread
I0922 16:50:37.515491   959 thread.cc:11] new thread exits

It looks like the default thread name is the program name. Actually if we take a look at pthread_setname_np(3), the manual says

By default, all the threads created using pthread_create() inherit the program name.

That looks simple. Let’s poke around to see how they’re implemented.

Implementation

static constexpr size_t kMaxThreadNameLength = 16;

bool setThreadName(std::thread::id tid, StringPiece name) {
  name = name.subpiece(0, kMaxThreadNameLength - 1);
  char buf[kMaxThreadNameLength] = {};
  std::memcpy(buf, name.data(), name.size());
  auto id = stdTidToPthreadId(tid);
  return 0 == pthread_setname_np(id, buf);
}

The other two setThreadName eventually calls the above function.

In pthread_setname_np(3), the manual says the length of name is restricted to 16 chars, including the null byte (‘\0’). That’s why this implementation creates an array of 16 bytes.

The confusing part here is the conversion between pthread_t, pid_t and std::thread::id.

If your Linux distribution is different, the header file location might be different, try to find it using

~ $ find /usr/include -name 'pthreadtypes.h'
/usr/include/x86_64-linux-gnu/bits/pthreadtypes.h

If we take a look at the header file on our local Linux machine, the definition is

typedef unsigned long int pthread_t;

And the std::thread::id is in /usr/include/c++/7/thread

/// thread
  class thread
  {
    typedef __gthread_t			native_handle_type;
    class id
    {
      native_handle_type	_M_thread;
      ...
    };

  private:
    id				_M_id;
  ...
}

where the __gthread_t is defined in /usr/include/x86_64-linux-gnu/c++/7/bits/. For POSIX, it’s an alias of pthread_t. So on POSIX compiliant system, std::thread::id is pthread_t.

As a library, how does folly guarantee the memcpy copies the same type on all platform? In stdTidToPthreadId, it has compile-time checks:

#if FOLLY_HAVE_PTHREAD && !_WIN32
pthread_t stdTidToPthreadId(std::thread::id tid) {
  static_assert(
      std::is_same<pthread_t, std::thread::native_handle_type>::value,
      "This assumes that the native handle type is pthread_t");
  static_assert(
      sizeof(std::thread::native_handle_type) == sizeof(std::thread::id),
      "This assumes std::thread::id is a thin wrapper around "
      "std::thread::native_handle_type, but that doesn't appear to be true.");
  ...
}
...