在多线程环境中使用 std::call_once() 进行初始化

眠りネ*_*ネロク 2 c++ multithreading initialization c++11 std-call-once

我正在阅读《C++ Concurrency in Action, 2nd Edition X》一书。本书包含一个示例,该示例将std::call_once()函数模板与对象一起使用,以线程安全的方式std::once_flag提供某种延迟初始化。

这是本书的简化摘录:

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_;
};
Run Code Online (Sandbox Code Playgroud)

上面的代码的作用是延迟连接的创建,直到客户端想要接收数据或有数据要发送为止。连接是由open_connection()私有成员函数创建的,而不是由 的构造函数创建的X。构造函数仅保存连接详细信息,以便能够在以后创建连接。

上面的成员函数只open_connection()被调用一次,到目前为止一切顺利。在单线程上下文中,这将按预期工作。但是,如果多个线程调用同一对象上的send_data()或成员函数怎么办?receive_data()

connection_显然,中数据成员的修改/更新与或open_connection()中的任何使用都不同步。send_data()receive_data()

是否会std::call_once()阻塞第二个线程,直到第一个线程从 中返回std::call_once()


第3.3.1在初始化期间保护共享数据

眠りネ*_*ネロク 5

基于这篇文章,我创建了这个答案。

我想看看是否与同一对象上std::call_once()的其他调用同步。以下程序创建了多个线程,这些线程调用一个函数,该函数包含一个使调用线程长时间休眠的调用。std::call_once()std::once_flagstd::call_once()

#include <mutex>

std::once_flag init_flag;
std::mutex mtx; 
Run Code Online (Sandbox Code Playgroud)

init_flagstd::once_flag与调用一起使用的对象std::call_once()。互斥锁mtx只是为了避免在从不同线程流式std::cout传输字符时出现交错输出std::cout

init()函数是由 调用的函数std::call_once()。它显示文本initialising...,使调用线程休眠三秒钟,然后done在返回之前显示文本:

#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';
   }
}
Run Code Online (Sandbox Code Playgroud)

该函数的目的是休眠足够长的时间(在本例中为三秒),以便剩余线程有足够的时间来到达调用std::call_once()。这样我们就能够看到它们是否阻塞,直到执行该函数的线程从中返回。

该函数do_work()由在以下位置创建的所有线程调用main()

void do_work() {
   std::call_once(init_flag, init);
   print_thread_id(); 
}
Run Code Online (Sandbox Code Playgroud)

init()只会被一个线程调用(即,只会被调用一次)。所有线程都调用print_thread_id(),即,为 中创建的每个线程执行一次main()

简单print_thread_id()地显示当前线程ID:

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

do_work()总共创建了16 个调用该函数的线程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();
}
Run Code Online (Sandbox Code Playgroud)

我在系统上得到的输出是:

initialising...done
0x7000054a9000
0x700005738000
0x7000056b5000
0x700005632000
0x700005426000
0x70000552c000
0x7000055af000
0x7000057bb000
0x70000583e000
0x7000058c1000
0x7000059c7000
0x700005a4a000
0x700005944000
0x700005acd000
0x700005b50000
0x700005bd3000
Run Code Online (Sandbox Code Playgroud)

此输出意味着在print_thread_id()第一个调用的线程std::call_once()从该线程返回之前,不会执行任何线程。这意味着这些线程在std::call_once()调用时被阻塞。