在std::call_once之前检查initialized
是否安全?它是否减少了同步开销?下面是我的代码:
struct Data {
std::vector<int> data;
std::once_flag flag;
bool initialized = false;
};
int run(Data& d, int l, int r) {
if (!d.initialized) {
std::call_once(d.flag, [&d]() {
d.data.resize(5);
std::generate(d.data.begin(), d.data.end(), [g = std::mt19937(std::random_device{}())]() mutable {
return g() % 10;
});
d.initialized = true;
});
}
// read only ops:
assert(d.data.size() == 5);
return std::accumulate(d.data.cbegin() + l, d.data.cbegin() + r, 0);
}
void func(Data& d, int l, int r) {
int res = 0;
for (int i = l; i < r; i++) {
res += run(d, i, r);
}
std::cout << l << ',' << r << ':' << res << '\n';
}
int main() {
Data data;
std::thread t1(func, std::ref(data), 1, 2);
std::thread t2(func, std::ref(data), 0, 3);
std::thread t3(func, std::ref(data), 1, 4);
t1.join();
t2.join();
t3.join();
}
字符串
2条答案
按热度按时间envsm3lx1#
一个线程可能正在阅读
if (!d.initialized)
中的d.initialized
,而另一个线程正在执行一次调用函数,写入d.initialized = true;
中的d.initialized
。这是一个数据竞争,因此会导致未定义的行为。你不能添加外部检查来验证初始化。
call_once
的全部意义在于以线程安全的方式提供这一点。如果你正确地手动实现了这样的检查,你将复制它的功能。在你的具体例子中,最好在启动线程之前直接在main中执行call-once函数。所有线程都需要等待实际执行call-once函数的线程。
ahy6op9u2#
我不认为这是不安全的,但它不是标准的。至少你应该使用mo_relaxed来保证原子性。
下面是仅使用call_once+once_flag生成的汇编代码(在x86-64 gcc -O3环境中)。
字符串
在检查时,我注意到缺少类似于测试和返回的优化逻辑。这种结果有点出乎意料。在其他环境中生成代码可能会产生不同的结果。GCC没有优化这种情况是因为它很罕见。解决您的第二个问题,添加的bool变量确实减轻了没有争用的情况下的开销
或者,考虑使用静态本地对象来满足您的需求。正如在以前的文章中提到的,使用函数进行静态初始化可以确保线程安全和初始化安全,同时保持高度优化。GCC采用了一种巧妙的技术来实现静态初始化。如果您感兴趣,可以进一步研究细节。我将在这里避免详细说明。