c++ 在call_once之前检查一个bool变量是否安全?

z3yyvxxp  于 12个月前  发布在  其他
关注(0)|答案(2)|浏览(75)

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

字符串

envsm3lx

envsm3lx1#

一个线程可能正在阅读if (!d.initialized)中的d.initialized,而另一个线程正在执行一次调用函数,写入d.initialized = true;中的d.initialized。这是一个数据竞争,因此会导致未定义的行为。
你不能添加外部检查来验证初始化。call_once的全部意义在于以线程安全的方式提供这一点。如果你正确地手动实现了这样的检查,你将复制它的功能。
在你的具体例子中,最好在启动线程之前直接在main中执行call-once函数。所有线程都需要等待实际执行call-once函数的线程。

ahy6op9u

ahy6op9u2#

我不认为这是不安全的,但它不是标准的。至少你应该使用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采用了一种巧妙的技术来实现静态初始化。如果您感兴趣,可以进一步研究细节。我将在这里避免详细说明。

相关问题