gpt4 book ai didi

c++ - 使用 4 个线程获取/释放语义

转载 作者:IT老高 更新时间:2023-10-28 22:04:20 25 4
gpt4 key购买 nike

我目前正在阅读 Anthony Williams 的 C++ Concurrency in Action。他的一个 list 显示了这段代码,他声明了 z != 0 的断言。可以火。

#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;
std::atomic<int> z;

void write_x()
{
x.store(true,std::memory_order_release);
}

void write_y()
{
y.store(true,std::memory_order_release);
}

void read_x_then_y()
{
while(!x.load(std::memory_order_acquire));
if(y.load(std::memory_order_acquire))
++z;
}

void read_y_then_x()
{
while(!y.load(std::memory_order_acquire));
if(x.load(std::memory_order_acquire))
++z;
}

int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load()!=0);
}

所以不同的执行路径,我能想到的是:

1)

Thread a (x is now true)
Thread c (fails to increment z)
Thread b (y is now true)
Thread d (increments z) assertion cannot fire


2)

Thread b (y is now true)
Thread d (fails to increment z)
Thread a (x is now true)
Thread c (increments z) assertion cannot fire


3)

Thread a (x is true)
Thread b (y is true)
Thread c (z is incremented) assertion cannot fire
Thread d (z is incremented)


有人可以向我解释这个断言是如何触发的吗?

他展示了这个小图形:
Image

应该不是商店到 y还与 read_x_then_y 中的负载同步,和店铺到 x与负载同步 read_y_then_x ?我很困惑。

编辑:

感谢您的回复,我了解原子是如何工作的以及如何使用获取/释放。我只是没有得到这个具体的例子。我试图弄清楚如果断言触发,那么每个线程做了什么?如果我们使用顺序一致性,为什么断言永远不会触发。

顺便说一下,我对此的推理是,如果 thread a ( write_x ) 存储到 x然后它到目前为止所做的所有工作都与读取 x 的任何其他线程同步。与获取订购。曾经 read_x_then_y看到这一点,它跳出循环并读取 y .现在,可能会发生两件事。在一种选择中, write_y已写信给 y , 表示此版本将与 if 语句 (load) 同步,意思是 z递增并且断言不能触发。另一个选项是如果 write_y还没有运行,这意味着 if 条件失败并且 z 没有增加,在这种情况下,只有 x是真的和 y仍然是假的。一旦 write_y 运行,read_y_then_x 就会跳出它的循环,但是两者都是 xy是真的和 z递增并且断言不会触发。我想不出任何“运行”或内存排序在哪里 z永远不会增加。有人可以解释我的推理哪里有缺陷吗?

另外,我知道循环读取将始终在 if 语句读取之前,因为获取阻止了这种重新排序。

最佳答案

您正在考虑顺序一致性,即最强(和默认)的内存顺序。如果使用这个内存顺序,所有对原子变量的访问构成一个全序,断言确实不能触发。

然而,在这个程序中,使用了较弱的内存顺序(释放存储和获取加载)。这意味着,根据定义,您不能假设操作的总顺序。特别是,您不能假设更改对其他线程以相同的顺序可见。 (对于任何原子内存顺序,仅保证每个单独变量的总顺序,包括 memory_order_relaxed 。)

本店至xy发生在不同的线程上,它们之间没有同步。 x的负载和 y发生在不同的线程上,它们之间没有同步。这意味着完全允许线程 c 看到 x && ! y和线程 d 看到 y && ! x . (我只是在此处缩写了获取加载,不要将此语法视为顺序一致的加载。)

底线:一旦您使用比顺序一致更弱的内存顺序,您就可以亲吻所有原子的全局状态的概念,即所有线程之间一致,再见。这正是为什么这么多人建议坚持顺序一致性的原因,除非您需要性能(顺便说一句,请记住衡量它是否更快!)并且确定您在做什么。另外,获得第二意见。

现在,你是否会被这个烧伤,是一个不同的问题。该标准仅允许基于用于描述标准要求的抽象机器的断言失败的场景。但是,由于某种原因,您的编译器和/或 CPU 可能不会利用此限额。因此,对于给定的编译器和 CPU,您可能永远不会在实践中看到触发了断言。请记住,编译器或 CPU 可能总是使用比您要求的更严格的内存顺序,因为这永远不会违反标准的最低要求。它可能只会让您损失一些性能——但无论如何标准都没有涵盖这一点。

更新以回应评论:该标准没有定义一个线程看到另一个线程对原子的更改所需的时间上限。向实现者建议值最终应该变得可见。

有顺序保证,但与您的示例相关的保证不会阻止断言触发。基本的获取-释放保证是,如果:

  • 线程 e 对原子变量执行释放存储 x
  • 线程 f 从同一个原子变量
  • 执行获取加载
  • 然后,如果 f 读取的值是 e 存储的值,则 e 中的存储与 f 中的负载同步。这意味着 e 中的任何(原子和非原子)存储,在此线程中,在给定存储之前排序到 x , 对 f 中的任何操作都是可见的,也就是说,在这个线程中,在给定的负载之后排序。 [请注意,除了这两个线程之外,不提供任何保证!]

  • 因此,与 e.g. 相反,不能保证 f 会读取 e 存储的值。 x 的一些旧值.如果它不读取更新的值,那么加载也不会与存储同步,并且上面提到的任何依赖操作都没有顺序保证。

    我将具有较少内存顺序的原子比作与相对论一致的顺序,其中有 no global notion of simultaneousness .

    PS:也就是说,原子负载不能只是读取任意的旧值。例如,如果一个线程执行 atomic<unsigned> 的周期性增量(例如使用释放顺序)变量,初始化为 0,另一个线程周期性地从这个变量加载(例如,使用获取顺序),然​​后,除了最终包装外,后一个线程看到的值必须单调增加。但这遵循给定的排序规则:一旦后一个线程读取 5,在从 4 到 5 的增量之前发生的任何事情都是在读取 5 之后的任何事情的相对过去。 事实上,除了换行之外的减少是甚至不允许 memory_order_relaxed ,但是这个内存顺序并没有对访问其他变量的相对顺序(如果有的话)做出任何 promise 。

    关于c++ - 使用 4 个线程获取/释放语义,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48383867/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com