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

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

本文关键字:call once 初始化 std 环境 多线程      更新时间:2023-10-16

我正在阅读C++并发在行动,第 2 版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()成员函数,该怎么办?

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

std::call_once()阻塞第二个线程,直到第一个线程从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()调用。这样,我们将能够看到它们是否阻塞,直到执行此函数的线程从中返回。

函数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

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