原子写入和易失性读取

atomic writes and volatile reads

我正在设计一个多线程算法,其中的要求是读取共享变量的最新值。对变量的写入是原子的(使用比较和交换)。但是,读取不是原子的。

考虑以下示例:

//Global variable 
int a = 10;


// Thread T1
void func_1() {
     __sync_bool_compare_and_swap(&a, 10, 100);
}

// Thread T2
void func_2() {
     int c = a;
     /* Some Operations */
     int b = a;
     /* Some Operations */
} 

如果代码 int b = a 在 func_1 中的 __sync_bool_compare_and_swap 之后(由线程 T2)执行(由线程 T2),那么根据我的理解,仍然不能保证读取 "variable a" 的最新值,因为编译器可以缓存 "a" 并使用 "a".

的旧值

现在,为了避免这个问题,我声明变量 "volatile" 如下:

volatile int a = 10;

// Thread T1
void func_1() {
     __sync_bool_compare_and_swap(&a, 10, 100);
}

// Thread T2
void func_2() {
     volatile int c = a;
     /* Some Operations */
     volatile int b = a;
     /* Some Operations */
} 

对于线程T1执行完__sync_bool_compare_and_swap后线程T2执行int b = a的相同场景,是否保证读取到"a"的最新值?

缓存一致性和内存一致性模型如何影响原子写入后的易失性读取?

volatile 关键字仅确保编译器不会将变量存储在寄存器中,而是在每次使用时从内存中加载变量。它与 运行 所在系统的缓存或内存一致性模型无关。

volatile 不会使读取操作原子化。 Non-atomic 读取与(原子)写入并发导致未定义的行为。使用任何形式的原子读取,std::atomic 或内在函数。不要将 volatile 用于任何形式的并发。

原子读取本身并不能保证该值将是最新。在您的情况下,理论上线程 T2 可能永远不会读取 100。该标准表示,实现(硬件、OS 等)应该尽最大努力使写入在有限时间内对其他线程可见。或许,不可能在这里提出正式的要求。

通过额外的同步,您可以获得更多的限制行为:

std::atomic<int> a = 10;
std::atomic<bool> done = false;

void func_1() {
    int old = 10;
    if (a.compare_exchange_strong(old, 100))
        done.store(true);
}

void func_2() {
    bool is_done = done.load();
    int b = a.load();
    assert(b == 100 || !is_done);

    while (!done.load()); // May spin indefinitely long, but should not do that
    assert(a.load() == 100);
}

实际上,要捕获那个简单的原子读取读取的不是 latest 值,就必须在程序中加入足够的同步(以定义 latest) 所以它看起来工作正常。

在您可能使用的所有支持 C++ 和多线程的平台上,从 volatile-qualified 对齐的 int 读取将是原子的,并且将读取最新值。但是,C++ 标准绝对不能保证这一点。它可能在某些平台上不起作用,并且可能不适用于下一个 CPU、编译器版本或 OS 版本。

理想情况下,使用保证提供原子性和可见性的东西。 C++-11 atomic 可能是最好的选择。编译器内在函数将是下一个最佳选择。如果你别无选择,只能使用 volatile,我建议你使用预处理器测试来确认你在一个已知足够的平台上并发出错误(使用 #error ) 如果不是。

请注意,在您可能使用的每个平台上,CPU 内存缓存完全无关紧要,因为硬件缓存一致性使它们不可见。在您可能使用的所有平台上,问题只是编译器优化、预取读取和发布写入。