原子读操作会导致死锁吗?

Can atomic read operations lead to deadlocks?

我正在看 this Herb Sutter's talk 有关原子、互斥和内存屏障的内容,我对此有疑问。由于 47:33 Herb 解释了互斥锁和原子与内存排序的关系。他在 49:12 上说,对于默认的内存顺序,即 memory_order_seq_cst,原子 load() 操作相当于锁定互斥锁,而原子 store 操作相当于解锁一个互斥体。在 53:15 上有人问 Herb 如果他的示例中的原子 load() 操作后面没有后续的 store() 操作会发生什么,他的回答让我很困惑:

54:00 - 如果您不写此版本(mutex.unlock()) 或此版本(atomic.store()), 那个人将永远没有机会 运行...

可能是因为我的英语水平不是很好,我完全误解了他,但我是这样理解他的话的:

如果一个线程 A 读取一个原子变量而没有后续写入它,没有其他线程能够使用这个变量,因为它被线程 [=21] 死锁了=] 就好像一个互斥锁被这个线程锁定了,没有进一步解锁。

但这真的是他的意思吗?这似乎不是真的。在我的理解中,释放会在加载或存储原子变量后立即自动发生。举个例子:

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> number{0};

void foo()
{
    while (number != 104) {}
    std::cout << "Number:\t" << number << '\n';
}

int main()
{
    std::thread thr1(foo);
    std::thread thr2(foo);
    std::thread thr3(foo);
    std::thread thr4(foo);
    number = 104;
    thr1.join();
    thr2.join();
    thr3.join();
    thr4.join();
}

在上面的示例中,有 4 个线程成功读取了同一个原子变量,并且这些线程中不需要任何写入来为其他线程释放变量。显然,atomic.load() != mutex.lock() 以及 atomic.store() != mutex.unlock()。它们在内存屏障方面的行为可能相同,但它们并不相同,不是吗?

你能给我解释一下吗,Herb 说它们相等到底是什么意思?

这里有个误会。无论内存顺序如何,原子读取 not“等同于锁定互斥体”。就 可见性而言 它可能具有相同的效果,但互斥体要重得多。

这是一个典型的互斥锁问题:

std::mutex mtx1;
std::mutex mtx2;

void thr1() {
    mtx1.lock();
    mtx2.lock();
    mtx2.unlock();
    mtx1.unlock();
}

void thr2() {
    mtx2.lock();
    mtx1.lock();
    mtx1.unlock();
    mtx2.unlock();
}

请注意,这两个函数以相反的顺序锁定两个互斥量。所以有可能 thr1 锁定 mtx1,然后 thr2 锁定 mtx2,然后 thr1 尝试锁定 mtx2 并且 thr2 尝试锁定 mtx1。这是一个僵局;两个线程都无法取得进展,因为它需要的资源被另一个线程占有。

原子没有这个问题,因为你不能运行在原子访问“内部”编写代码。你不会遇到那种资源冲突。

这个讨论背后的问题似乎是线程 运行ning while (number != 104) {} 可能看不到 number 的更新值,因此代码将是一个无限循环。这不是僵局。这并不是说这不是问题,而是可见性的问题。