通过一个线程写和另一个非原子读保证写

问题描述

我有

bool unsafeBool = false;

int main()
{
    std::thread reader = std::thread([](){ 
        std::this_thread::sleep_for(1ns);
        if(unsafeBool) 
            std::cout << "unsafe bool is true" << std::endl; 
    });
    std::thread writer = std::thread([](){ 
        unsafeBool = true; 
    });
    reader.join();
    writer.join();
}

是否可以保证写完后unsafeBool成为true。我知道读取器输出的是不确定的行为,但据我所知应该写得很好。

解决方法

writer.join()之后,它保证unsafeBool == true。但是在阅读器线程中,对其的访问是一场数据竞赛。

,

UB是UB,现在仍然是UB,虽然有理由说明事情为何会阻碍事情的发展,但是您不能依赖它。

您有一个竞争条件,可以通过以下方法解决:添加锁或将类型更改为原子。

由于代码中包含UB,因此允许编译器假定不会发生这种情况。如果它能够检测到此错误,则可以将您的完整功能更改为noop,因为它永远无法在有效程序中调用。

如果不这样做,其行为将取决于您的处理器以及与其链接的缓存。在那里,如果联接之后的代码使用与读取布尔值的线程相同的内核(在联接之前),那么您甚至可能在其中仍然有false,而无需使缓存无效。

在实践中,使用Intel X86处理器,您不会在竞争条件下看到很多副作用,因为它已经使写入时的缓存失效。

,

某些实现方式可以保证,任何尝试在其变化期间读取不合格volatile的字号较小的对象的值的尝试都会产生旧值或新值,可以任意选择。在这种保证很有用的情况下,编译器始终如一地坚持下去的成本通常会低于解决缺失的成本(此外,因为程序员可以通过任何方式解决缺失的成本都会限制编译器的成本。在旧值或新值之间自由选择)。

但是,在其他一些实现中,即使看起来好像它们应该涉及值的单次读取的操作也可能会产生将多次读取的结果组合在一起的代码。当使用命令行参数-xc -O2 -mcpu=cortex-m0调用ARM gcc 9.2.1并给出以下命令时:

#include <stdint.h>
#include <string.h>
#if 1
uint16_t test(uint16_t *p)
{
    uint16_t q = *p;
    return q - (q >> 15);
}

它生成的代码先从*p读取,然后从*(int16_t*)p读取,将后者右移15,并将其添加到前者中。如果在两次读取之间更改*p的值,则可能导致函数返回0xFFFF,该值应该是不可能的。

不幸的是,许多设计编译器以使他们始终避免以这种方式“拆分”阅读的人认为,这种行为是足够自然和明显的,因此没有特别的理由明确表明他们从不做其他事情。同时,其他一些编译器作者认为,由于该标准允许编译器在没有理由的情况下拆分读取,即使在没有理由的情况下(将上面的代码拆分为读取,这会使它变得更大,更慢)。将依赖编译器避免这种“优化”被“破坏”。