在多线程环境中使用 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()
?
X第 3.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::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()
调用。这样我们就可以看到它们是否阻塞,直到线程从中执行这个函数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()
调用时被阻塞。
我正在阅读 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()
?
X第 3.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::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()
调用。这样我们就可以看到它们是否阻塞,直到线程从中执行这个函数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()
调用时被阻塞。