c++multithreadingc++11initializationstd-call-once

Initialising with std::call_once() in a multithreading environment


I'm reading the book C++ Concurrency in Action, 2nd Edition X. The book contains an example that uses the std::call_once() function template together with an std::once_flag object to provide some kind of lazy initialisation in thread-safe way.

Here a simplified excerpt from the book:

class X {
public:
   X(const connection_details& details): connection_details_{details}
   {}

   void send_data(const data_packet& data) {
      std::call_once(connection_init_, &X::open_connection, this);
      connection_.send(data); // connection_ is used
   }

   data_packet receive_data() {
      std::call_once(connection_init_, &X::open_connection, this);
      return connection_.recv(data); // connection_ is used
   }

private:
   void open_connection() {
      connection_.open(connection_details_); // connection_ is modified
   }

   connection_details connection_details_;
   connection_handle connection_;
   std::once_flag connection_init_;
};

What the code above does, is to delay the creation of the connection until the client wants to receive data or has data to send. The connection is created by the open_connection() private member function, not by the constructor of X. The constructor only saves the connection details to be able to create the connection at some later point.

The open_connection() member function above is called only once, so far so good. In a single-threaded context, this will work as expected. However, what if multiple threads are calling either the send_data() or the receive_data() member function on the same object?

Apparently, the modification/update of the connection_ data member in open_connection() is not synchronised with any of its uses in send_data() or receive_data().

Does std::call_once() block a second thread until the first one returns from std::call_once()?


XSection 3.3.1.: Protecting shared data during initialization


Solution

  • Based on this post I've created this answer.

    I wanted to see whether std::call_once() synchronises with other calls to std::call_once() on the same std::once_flag object. The following program creates several threads that call a function that contains a call to std::call_once() that puts the calling thread to sleep for long time.

    #include <mutex>
    
    std::once_flag init_flag;
    std::mutex mtx; 
    

    init_flag is the std::once_flag object to be used with the std::call_once() call. The mutex mtx is just for avoiding interleaved output on std::cout when streaming characters into std::cout from different threads.

    The init() function is the one called by std::call_once(). It displays the text initialising..., puts the calling thread to sleep for three seconds and then displays the text done before returning:

    #include <thread>
    #include <chrono>
    #include <iostream>
    
    void init() {
       {
          std::lock_guard<std::mutex> lg(mtx);
          std::cout << "initialising...";
       }
    
       std::this_thread::sleep_for(std::chrono::seconds{3});  
    
       {
          std::lock_guard<std::mutex> lg(mtx);
          std::cout << "done" << '\n';
       }
    }
    

    The purpose of this function is to sleep for long enough (three seconds in this case), so that the remaining threads have enough time to reach the std::call_once() call. This way we will be able to see whether they block until the thread executing this function returns from it.

    The function do_work() is called by all threads that are created in main():

    void do_work() {
       std::call_once(init_flag, init);
       print_thread_id(); 
    }
    

    init() will be only called by one thread (i.e., it will be called only once). All threads call print_thread_id(), i.e., it is executed once for every thread created in main().

    The print_thread_id() simply displays the current thread id:

    void print_thread_id() {
       std::lock_guard<std::mutex> lg(mtx);
       std::cout << std::this_thread::get_id() << '\n';
    }
    

    A total of 16 threads, which call the do_work() function, are created in main():

    #include <vector>
    
    int main() {
       std::vector<std::thread> threads(16);
       for (auto& th: threads)
          th = std::thread{do_work};
    
       for (auto& th: threads)
          th.join();
    }
    

    The output I get on my system is:

    initialising...done
    0x7000054a9000
    0x700005738000
    0x7000056b5000
    0x700005632000
    0x700005426000
    0x70000552c000
    0x7000055af000
    0x7000057bb000
    0x70000583e000
    0x7000058c1000
    0x7000059c7000
    0x700005a4a000
    0x700005944000
    0x700005acd000
    0x700005b50000
    0x700005bd3000
    

    This output means that no thread executes print_thread_id() until the first thread that called std::call_once() returns from it. This implies that those threads are blocked at the std::call_once() call.