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

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

我正在阅读 C++ Concurrency in Action,第 2 版 X 一书。本书包含一个示例,该示例使用 std::call_once() function template together with an 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_;
};

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

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

显然,open_connection()connection_ 数据成员的 modification/update 与其在 send_data()receive_data() 中的任何使用不同步。

std::call_once() 是否会阻塞第二个线程,直到第一个线程 returns 来自 std::call_once()


X3.3.1. 节:初始化期间保护共享数据

我根据 创建了这个答案。

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

#include <mutex>

std::once_flag init_flag;
std::mutex mtx; 

init_flag 是要与 std::call_once() 调用一起使用的 std::once_flag 对象。互斥量 mtx 只是为了避免在将字符从不同线程流式传输到 std::coutstd::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';
   }
}

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

函数 do_work()main() 中创建的所有线程调用:

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

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';
}

一共创建了16个线程,调用了do_work()函数,在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();
}

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

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

此输出意味着没有线程执行 print_thread_id() 直到第一个线程从它调用 std::call_once() returns 。这意味着这些线程在 std::call_once() 调用时被阻塞。