Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is std::call_once reentrant and thread safe?

Tags:

c++

c++11

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.

like image 556
rgoble Avatar asked Mar 27 '14 15:03

rgoble


People also ask

Is std :: Call_once thread safe?

The CPP Reference states std::call_once is thread safe: Executes the function f exactly once, even if called from several threads.

What is std :: Call_once?

std::call_once ensures execution of a function exactly once by competing threads. It throws std::system_error in case it cannot complete its task.


1 Answers

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.

like image 62
ecatmur Avatar answered Nov 15 '22 17:11

ecatmur