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

时间:2019-05-12 12:15:12

标签: c++ multithreading c++11 initialization std-call-once

我正在阅读《第二​​版C ++并发行动》 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_;
};

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

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

很明显,connection_open_connection()数据成员的修改/更新与其在send_data()receive_data()中的任何使用都不同步。

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


X 部分 3.3.1。在初始化期间保护共享数据

1 个答案:

答案 0 :(得分:0)

基于this post,我创建了此答案。

我想查看std::call_once()是否与在同一std::call_once()对象上对std::once_flag的其他调用同步。以下程序创建了几个线程,这些线程调用一个函数,该函数包含对std::call_once()的调用,该调用使调用线程长时间处于睡眠状态。

#include <mutex>

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

init_flag是要与std::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';
   }
}

此功能的目的是睡眠足够长的时间(在这种情况下为三秒钟),以便其余线程有足够的时间来到达std::call_once()调用。这样,我们将能够查看它们是否阻塞,直到执行此函数的线程从中返回为止。

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

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();
}

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

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

此输出意味着直到调用print_thread_id()的第一个线程从中返回之前,没有线程执行std::call_once()。这意味着这些线程在std::call_once()调用中被阻塞。