提问人:mian 提问时间:11/17/2023 更新时间:11/17/2023 访问量:143
它是否安全,我可以从call_once之前检查布尔变量中受益吗
Is it safe and can I get benefit from checking a bool variable before call_once
问:
检查之前的 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();
}
答:
4赞
user17732522
11/17/2023
#1
一个线程可能正在读入,而另一个线程正在执行写入 in 的调用一次函数。这是一场数据竞赛,因此会导致未定义的行为。d.initialized
if (!d.initialized)
d.initialized
d.initialized = true;
您无法添加外部检查来验证初始化。重点是以线程安全的方式提供这一点。如果您手动正确地实施了此类检查,您将复制其功能。call_once
在您的特定示例中,最好在启动线程之前直接在 main 中执行 call-once 函数。无论哪种方式,所有线程都需要等待实际执行一次调用函数的线程。
评论
0赞
mian
11/17/2023
如果向量初始化不影响指令重新排序,则一个线程可能会使断言失败?
2赞
user17732522
11/17/2023
@mian我不明白你的意思。如果没有断言,则保证不会失败。使用该程序具有未定义的行为,任何事情都可能发生。d.initialized
d.initialized
0赞
mian
11/17/2023
我的意思是,是否有可能一个线程在实际初始化向量之前运行,而另一个线程可能读取 d.initialized 为 true,但发现向量的大小不正确。d.initialized = true;
1赞
Sebastian Redl
11/17/2023
@mian 程序具有未定义的行为。一个线程可以读取为 ,然后继续执行函数的其余部分,跳过 .d.initialized
false
call_once
2赞
user17732522
11/17/2023
@mian 未定义的行为意味着您对任何行为都无法保证。因此,任何“可能”问题的答案都是“是”。它也可能崩溃或只是完全跳过代码或其他任何内容。
-1赞
kcbsbo
11/17/2023
#2
我不认为这是不安全的,但这不是标准的。至少你应该使用mo_relaxed来保证原子性。
以下是仅使用 call_once+once_flag 生成的汇编代码(在 x86-64 gcc -O3 环境中)。
push rbp
push rbx
sub rsp, 24
mov rbp, QWORD PTR std::__once_callable@gottpoff[rip]
mov rbx, QWORD PTR std::__once_call@gottpoff[rip]
lea rax, [rsp+7]
mov QWORD PTR [rsp+8], rax
lea rax, [rsp+8]
mov QWORD PTR fs:[rbp+0], rax
mov eax, OFFSET FLAT:_ZL28__gthrw___pthread_key_createPjPFvPvE
mov QWORD PTR fs:[rbx], OFFSET FLAT:_ZZNSt9once_flag18_Prepare_executionC4IZSt9call_onceIZ14simple_do_oncevEUlvE_JEEvRS_OT_DpOT0_EUlvE_EERS5_ENUlvE_4_FUNEv
test rax, rax
je .L13
mov esi, OFFSET FLAT:__once_proxy
mov edi, OFFSET FLAT:flag1
call __gthrw_pthread_once(int*, void (*)())
test eax, eax
jne .L10
mov QWORD PTR fs:[rbp+0], 0
mov QWORD PTR fs:[rbx], 0
add rsp, 24
pop rbx
pop rbp
ret
经过检查,我注意到缺少类似于测试和返回的优化逻辑。这个结果有些出乎意料。在其他环境中生成代码可能会产生不同的结果。GCC 可能由于其稀有性而没有优化这种情况。针对您的第二个问题,添加的 bool 变量确实可以在没有争用的场景中减轻开销
或者,考虑使用静态本地对象来满足您的要求。如上一篇文章所述,使用函数进行静态初始化可确保线程安全和初始化安全,同时保持高度优化。GCC 采用一种巧妙的技术来实现静态初始化。如果您有兴趣,可以进一步调查详细信息。我不会在这里详细说明这一点。
评论
0赞
user17732522
11/18/2023
即使这还不是 UB,我很确定任何体面的实现都会在进入慢速路径之前进行快速原子检查,例如参见 glibc 的 github.com/bminor/glibc/blob/master/nptl/pthread_once.c#L139(例如 libstdc++ 的 )。正如你所看到的,检查也至少需要是获取,存储是发布。否则,它们不会在调用一次函数中的计算上建立任何内存排序。编译器可以自由地对它们进行重新排序。std::call_once
std::call_once
0赞
user17732522
11/18/2023
在所示的程序集中,快速路径逻辑隐藏在 后面。它在链接时链接到不同的实现,具体取决于应用程序是作为单线程还是多线程链接。__gthrw_pthread_once
评论
initialized
std::call_once