rgoble rgoble - 3 months ago 22
C++ Question

Is std::call_once reentrant and thread safe?

std::call_once is thread safe, but is it re-entrant as well?

My testing using VS2012 (Debug & Release) has shown that calling

std::call_once
recursively from a single thread is okay, but if the calls are made on separate threads it will cause a deadlock. Is this a known limitation of
std::call_once
?

#include "stdafx.h"

#include <iostream>
#include <mutex>
#include <thread>

void Foo()
{
std::cout << "Foo start" << std::endl;

std::once_flag flag;
std::call_once( flag, [](){
std::cout << "Hello World!" << std::endl;
});

std::cout << "Foo end" << std::endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
// Single threaded Works
{
std::once_flag fooFlag;
std::call_once( fooFlag, Foo);
}

// Works
// Threaded version, join outside call_once
{
std::once_flag fooFlag;
std::thread t;
std::call_once( fooFlag, [&t](){
t = std::thread(Foo);
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
});
t.join();
}

// Dead locks
// Threaded version, join inside call_once
{
std::once_flag fooFlag;
std::call_once( fooFlag, [](){
auto t = std::thread(Foo);
t.join();
});
}

return 0;
}


It seems like
std:call_once
is locking a static mutex that doesn't get unlocked until the function exits. In the single-threaded case it works because on the second call that thread already has the lock. On the threaded version it will block until the first call exits.

I also noticed that if you change the
std::once_flag
flag in the
Foo()
function to
static
that the deadlock will still occur.

Answer

The closest the standard comes to specifying this is 17.6.5.8 [reentrancy]:

1 - Except where explicitly specified in this standard, it is implementation-defined which functions in the Standard C ++ library may be recursively reentered.

Unfortunately the specification of call_once doesn't say whether it is recursive (or cross-thread recursive), and the thread support library preamble doesn't say anything on this topic either.

That said, the VC++ implementation is clearly suboptimal, especially as it's possible to write a userland version of call_once using condition_variable:

#include <mutex>
#include <condition_variable>

struct once_flag {
  enum { INIT, RUNNING, DONE } state = INIT;
  std::mutex mut;
  std::condition_variable cv;
};
template<typename Callable, typename... Args>
void call_once(once_flag &flag, Callable &&f, Args &&...args)
{
  {
    std::unique_lock<std::mutex> lock(flag.mut);
    while (flag.state == flag.RUNNING) {
      flag.cv.wait(lock);
    }
    if (flag.state == flag.DONE) {
      return;
    }
    flag.state = flag.RUNNING;
  }
  try {
    f(args...);
    {
      std::unique_lock<std::mutex> lock(flag.mut);
      flag.state = flag.DONE;
    }
    flag.cv.notify_all();
  }
  catch (...) {
    {
      std::unique_lock<std::mutex> lock(flag.mut);
      flag.state = flag.INIT;
    }
    flag.cv.notify_one();
    throw;
  }
}

Note that this is a fine-grained implementation; it's also possible to write a coarse-grained implementation that uses a single pair of mutex and condition variable across all once flags, but then you need to ensure that you notify all the waiting threads on throwing an exception (for example, libc++ does this).

For efficiency you could make once_flag::state an atomic and use double-checked locking; this is omitted here for conciseness.