最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Does this transitive happens-before use case need sequential consistency or acquire release will suffice? - Stack Overflow

programmeradmin6浏览0评论

This snip is from Herb Sutter's Atomic Weapons talk slide from page number 19.

If I am understanding this correctly, what Herb is saying is that, for the assert in thread 3 to not fire, this has to follow sequential consistency. So the following code will not fire the assert.

int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics

void thread1() {
    g = 1;
    x.store(1, std::memory_order_seq_cst);
}

void thread2() {
    if(x.load(std::memory_order_seq_cst) == 1)
        y.store(1, std::memory_order_seq_cst);
}

void thread3() {
    if(y.load(std::memory_order_seq_cst) == 1)
        assert( g == 1 );
}

But wouldn't this also not fire the assert if release/acquire was used instead as follows?

int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics

void thread1() {
    g = 1;  // A
    x.store(1, std::memory_order_release);
}

void thread2() {
    if(x.load(std::memory_order_acquire) == 1)
        y.store(1, std::memory_order_release);
}

void thread3() {
    if(y.load(std::memory_order_acquire) == 1)
        assert( g == 1 );  // B
}

Q1 - Doesn't //A simply-happens-before //B ensuring that the assert will not fire, since no other thread writes to g?

Q2 - Am I understanding the purport of the slide incorrectly, or something is wrong on the slide?

This snip is from Herb Sutter's Atomic Weapons talk slide from page number 19.

If I am understanding this correctly, what Herb is saying is that, for the assert in thread 3 to not fire, this has to follow sequential consistency. So the following code will not fire the assert.

int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics

void thread1() {
    g = 1;
    x.store(1, std::memory_order_seq_cst);
}

void thread2() {
    if(x.load(std::memory_order_seq_cst) == 1)
        y.store(1, std::memory_order_seq_cst);
}

void thread3() {
    if(y.load(std::memory_order_seq_cst) == 1)
        assert( g == 1 );
}

But wouldn't this also not fire the assert if release/acquire was used instead as follows?

int g{0}; // normal int
std::atomic<int> x{0}, y{0}; // atomics

void thread1() {
    g = 1;  // A
    x.store(1, std::memory_order_release);
}

void thread2() {
    if(x.load(std::memory_order_acquire) == 1)
        y.store(1, std::memory_order_release);
}

void thread3() {
    if(y.load(std::memory_order_acquire) == 1)
        assert( g == 1 );  // B
}

Q1 - Doesn't //A simply-happens-before //B ensuring that the assert will not fire, since no other thread writes to g?

Q2 - Am I understanding the purport of the slide incorrectly, or something is wrong on the slide?

Share Improve this question edited Feb 6 at 8:31 Dhwani Katagade asked Feb 5 at 10:54 Dhwani KatagadeDhwani Katagade 1,29215 silver badges28 bronze badges 4
  • 3 Yes, acq/rel looks safe here. – HolyBlackCat Commented Feb 5 at 10:59
  • @Renat The answer got fixed to no longer say that. – HolyBlackCat Commented Feb 5 at 12:02
  • I was just wrong originally. The alternating pattern of sequenced before/synchronizes with makes a happens before chain run through everything from g = 1 to g == 1. Yes, synchronization only happens on the same atomic variable, but what ultimately matters is "happens before", which can be established in multiple ways and between different objects, atomic or otherwise. – Jan Schultke Commented Feb 5 at 12:05
  • 2 As widely referenced as this talk is, it'd be nice if Herb or somebody else would maintain an errata list. – Nate Eldredge Commented Feb 5 at 15:56
Add a comment  | 

2 Answers 2

Reset to default 9

Yes, acquire/release ordering would be sufficient here.

With acquire/release ordering and g being non-atomic, the code is okay because

  • T1:g = 1 is sequenced before T1:x = 1
  • T1:x = 1 synchronizes with T2:x == 1
  • T2:x == 1 is sequenced before T2:y = 1
  • T2:y = 1 is synchronizes with T3:y == 1
  • T3:y = 1 is sequenced before T3:g == 1

You have a happens before relationship running all the way from T1:g = 1 to T3:g == 1, and this means that there is no data race and that T3 has to read 1 (see [intro.races] p13).

Obviously, Thread 3 could also "run first", synchronization doesn't happen, and this whole happens before chain doesn't exist. However, if T3:y == 1 is true, the synchronizations must have taken place, T1:g = 1 happens before T3:assert(g == 1), and the assertion cannot fail.

Herb's mistake (possibly)

In the talk, Herb mentions that g is non-atomic. That makes it really hard to tell why he thinks that sequential consistency is required, considering that sequentially consistent operations on x don't provide any relevant extra guarantees regarding g.

Perhaps if g was atomic, one might suspect that g == 1 wouldn't always be true because of [intro.races] p13:

The value of an atomic object M, as determined by evaluation B, is the value stored by some unspecified side effect A that modifies M, where B does not happen before A.

In other words, assert(g == 1) could read arbitrarily far into the past modifications of g (but not into the future). This is the rule that misled Herb into thinking that this example requires sequential consistency.

However, a happens before chain as described above also imposes requirements on atomics:

If a side effect X on an atomic object M happens before a value computation B of M, then the evaluation B takes its value from X or from a side effect Y that follows X in the modification order of M.

In our case, g = 1 happens before assert(g == 1), so the assertion has to pass (or take a value of g that is written after g = 1, but no such write exists in this sample code).


Note: a happens before chain is upheld because an alternating pattern of sequenced before and synchronizes with is covered by inter-thread happens before.

The example is important, but you're right the heading and discussion should be updated now that C++11 "acq/rel" meanings are well established.

This example can't affected by "ordinary acq/rel" vs "SC acq/rel" because the key difference between the two is that the former allows store-load reordering and the latter disallows it, as I covered a few slides earlier. This example has all the other three combinations, with one thread each for store-store, load-store, and load-load; but this example doesn't have store-load and so it's immune to the difference between those two acq/rel flavors.

The main point I was aiming for in this slide (whose second half covers a total-store-order example) is that for a memory model to be usable it can't just talk about individual threads' operations in isolation and sufficiently cover all cases. To have a consistent memory model a programmer can reason about, each thread's operations also have to work in an SC manner both transitively (when daisy-chained together like this) and globally (for a total store order, which was the other example on this slide), otherwise we can violate causality and humans can't write a coherent program.

I think the reason I mentioned "acq/rel" in the slide title is that during the development of the C++ MM there were many proposals for more relaxed rules than we standardized, including ideas about just letting individual pairs of acq/rel be enough without a transitive property, and those models are too relaxed to be useful. But I agree that since now "acq/rel" are now well-established to mean the m_o_acq and m_o_rel in the standard I should stick to those meanings of "acq/rel" to avoid confusion (they were still very new at the time I gave this talk and were still being implemented by compilers, but this was 2012 and I still should not have made them the issue here).

I've updated the slide title for the next time I give the talk. Thanks!

发布评论

评论列表(0)

  1. 暂无评论