具有4个线程的获取/释放语义 [英] Acquire/release semantics with 4 threads

查看:64
本文介绍了具有4个线程的获取/释放语义的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在阅读Anthony Williams撰写的C ++ Concurrency in Action.他的清单之一显示了此代码,并且他指出z != 0可以触发的断言.

I am currently reading C++ Concurrency in Action by Anthony Williams. One of his listing shows this code, and he states that the assertion that z != 0 can fire.

#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);
}

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

So the different execution paths, that I can think of is this:

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)

有人可以向我解释该断言如何触发吗?

Could someone explain to me how this assertion can fire?

他显示了这个小图形:

He shows this little graphic:

存储到y的存储区是否也与read_x_then_y中的负载同步,存储到x的存储区是否也与read_y_then_x中的负载同步?我很困惑

Shouldn't the store to y also sync with the load in read_x_then_y, and the store to x sync with the load in read_y_then_x? I'm very confused.

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

Thank you for your responses, I understand how atomics work and how to use Acquire/Release. I just don't get this specific example. I was trying to figure out IF the assertion fires, then what did each thread do? And why does the assertion never fire if we use sequential consistency.

我对此的理由是,如果thread a(write_x)存储到x,那么到目前为止已完成的所有工作都将与其他任何读取x的线程进行同步,并获取顺序.一旦read_x_then_y看到此内容,它就会跳出循环并读取y.现在,可能会发生2件事情.在一个选项中,write_y已写入y,这意味着此版本将与if语句(装入)同步,这意味着z递增且断言无法触发.另一个选项是write_y尚未运行,这意味着if条件失败并且z不会增加.在这种情况下,只有x为true,而y仍然为false.一旦运行write_y,read_y_then_x就会跳出其循环,但是xy均为true,并且z递增,并且断言不会触发.我想不出z永不递增的任何运行"或内存顺序.有人可以解释我的推理存在缺陷的地方吗?

The way, I am reasoning about this is that if thread a (write_x) stores to x then all the work it has done so far is synced with any other thread that reads x with acquire ordering. Once read_x_then_y sees this, it breaks out of the loop and reads y. Now, 2 things could happen. In one option, the write_y has written to y, meaning this release will sync with the if statement (load) meaning z is incremented and assertion cannot fire. The other option is if write_y hasn't run yet, meaning the if condition fails and z isn't incremented, In this scenario, only x is true and y is still false. Once write_y runs, the read_y_then_x breaks out of its loop, however both x and y are true and z is incremented and the assertion does not fire. I can't think of any 'run' or memory ordering where z is never incremented. Can someone explain where my reasoning is flawed?

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

Also, I know The loop read will always be before the if statement read because the acquire prevents this reordering.

推荐答案

您正在考虑顺序一致性,即最强(也是默认)的内存顺序.如果使用此内存顺序,则对原子变量的所有访问都会构成一个总顺序,并且确实不能触发该断言.

You are thinking in terms of sequential consistency, the strongest (and default) memory order. If this memory order is used, all accesses to atomic variables constitute a total order, and the assertion indeed cannot be triggered.

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

However, in this program, a weaker memory order is used (release stores and acquire loads). This means, by definition that you cannot assume a total order of operations. In particular, you cannot assume that changes become visible to other threads in the same order. (Only a total order on each individual variable is guaranteed for any atomic memory order, including memory_order_relaxed.)

xy的存储发生在不同的线程上,它们之间没有同步. xy的负载发生在不同的线程上,它们之间没有同步.这意味着完全允许线程c看到x && ! y,而线程d看到y && ! x. (我在这里只是缩写了acquire-loads,不要用这种语法来表示顺序一致的负载.)

The stores to x and y occur on different threads, with no synchronization between them. The loads of x and y occur on different threads, with no synchronization between them. This means it is entirely allowed that thread c sees x && ! y and thread d sees y && ! x. (I'm just abbreviating the acquire-loads here, don't take this syntax to mean sequentially consistent loads.)

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

Bottom line: Once you use a weaker memory order than sequentially consistent, you can kiss your notion of a global state of all atomics, that is consistent between all threads, goodbye. Which is exactly why so many people recommend sticking with sequential consistency unless you need the performance (BTW, remember to measure if it's even faster!) and are certain of what you are doing. Also, get a second opinion.

现在,您是否会因此而烦恼,是另一个问题.基于用于描述标准要求的抽象机,该标准仅允许断言失败的场景.但是,您的编译器和/或CPU可能由于某种原因而无法利用此配额.因此,实际上对于给定的编译器和CPU,您可能永远不会看到断言被触发.请记住,编译器或CPU可能总是使用 stricter 内存顺序而不是您要求的顺序,因为这永远不会违反标准的最低要求.它可能只会使您损失一些性能-但是无论如何,该标准并没有涵盖这部分.

Now, whether you will get burned by this, is a different question. The standard simply allows a scenario where the assertion fails, based on the abstract machine that is used to describe the standard requirements. However, your compiler and/or CPU may not exploit this allowance for one reason or another. So it is possible that for a given compiler and CPU, you may never see that the assertion is triggered, in practice. Keep in mind that a compiler or CPU may always use a stricter memory order than the one you asked for, because this can never introduce violations of the minimum requirements from the standard. It may only cost you some performance – but that is not covered by the standard anyway.

根据评论进行更新:该标准对一个线程看到另一个线程对原子的更改所花费的时间没有严格的上限.向实施者建议,值应最终可见.

UPDATE in response to comment: The standard defines no hard upper limit on how long it takes for one thread to see changes to an atomic by another thread. There is a recommendation to implementers that values should become visible eventually.

sequencing 保证,但是与您的示例相关的保证不能阻止断言的触发.基本的获取发布保证是:

There are sequencing guarantees, but the ones pertinent to your example do not prevent the assertion from firing. The basic acquire-release guarantee is that if:

  • 线程e对原子变量x
  • 执行释放存储
  • 线程f从相同的原子变量执行获取负载
  • 然后 if 由f读取的值是由e存储的值,e中的存储与f中的负载同步.这意味着,在给定存储x之前在该线程中 中的e中的任何(原子和非原子)存储对于f中的任何操作(即在该线程中,在给定负载之后进行排序. [请注意,除了这两个线程以外,没有其他保证!]
  • Thread e performs a release-store to an atomic variable x
  • Thread f performs an acquire-load from the same atomic variable
  • Then if the value read by f is the one that was stored by e, the store in e synchronizes-with the load in f. This means that any (atomic and non-atomic) store in e that was, in this thread, sequenced before the given store to x, is visible to any operation in f that is, in this thread, sequenced after the given load. [Note that there are no guarantees given regarding threads other than these two!]

因此,不能保证f 会读取e存储的值,而不是e. x的一些旧值.如果它读取更新的值,那么负载也与存储区同步,并且上述任何相关操作都无法保证顺序.

So, there is no guarantee that f will read the value stored by e, as opposed to e.g. some older value of x. If it doesn't read the updated value, then also the load does not synchronize with the store, and there are no sequencing guarantees for any of the dependent operations mentioned above.

我将原子顺序比相对论相对顺序一致的原子更喜欢,其中没有全局概念同步性.

I liken atomics with lesser memory order than sequentially consistent to the Theory of Relativity, where there is no global notion of simultaneousness.

PS:也就是说,原子加载不能只读取任意一个较旧的值.例如,如果一个线程执行atomic<unsigned>变量的周期性增量(例如,以释放顺序),并初始化为0,而另一个线程从该变量中周期性地加载(例如,以获取顺序),则除最终包装外,其他值后者所看到的线程必须单调增加.但这是从给定的排序规则得出的:一旦后一个线程读取了5,那么从4递增到5之前发生的任何事情都是相对于读取5之后的任何事情的相对过去.实际上,除包装外的减少是甚至不允许memory_order_relaxed但是,但是此内存顺序并不能保证对访问其他变量的相对顺序(如果有).

PS: That said, an atomic load cannot just read an arbitrary older value. For example, if one thread performs periodic increments (e.g. with release order) of an atomic<unsigned> variable, initialized to 0, and another thread periodically loads from this variable (e.g. with acquire order), then, except for eventual wrapping, the values seen by the latter thread must be monotonically increasing. But this follows from the given sequencing rules: Once the latter thread reads a 5, anything that happened before the increment from 4 to 5 is in the relative past of anything that follows the read of 5. In fact, a decrease other than wrapping is not even allowed for memory_order_relaxed, but this memory order does not make any promises to the relative sequencing (if any) of accesses to other variables.

这篇关于具有4个线程的获取/释放语义的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆