“释放顺序”是什么意思?

时间:2016-07-25 10:43:58

标签: c++ multithreading c++11 concurrency

我不明白,如果我们在下面的示例中有2个线程,为什么没有release sequence会出现问题。我们对原子变量count只有2个操作。 count将逐步递减,如输出中所示。

来自 Antony Williams的 C ++并发行动

  

我提到你可以在另一个线程的synchronizes-with relationship到原子变量和store原子变量之间获得load,即使有一个{{1的序列read-modify-writestore之间的操作,前提是所有操作都经过适当标记。如果商店标有loadmemory_order_releasememory_order_acq_rel,则加载标记为memory_order_seq_cstmemory_order_consumememory_order_acquire,并且链中的每个操作都加载上一个操作所写的值,然后操作链构成 发布序列 和初始存储memory_order_seq_cst(对于{ {1}}或synchronizes-with)或memory_order_acquire(对于memory_order_seq_cst)最终加载。链中的任何原子读 - 修改 - 写操作都可以有任何内存排序(甚至dependency-ordered-before)。

     

要查看这意味着什么(发布顺序)及其重要性,请考虑将memory_order_consume用作共享队列中项目数的计数,如下面的清单所示。

     

处理事情的一种方法是让生成数据的线程将项目存储在共享缓冲区中,然后执行memory_order_relaxed #1 让其他线程知道数据是可用。然后,在实际读取共享缓冲区#4 之前,使用队列项的线程可以atomic<int> #2 从队列中声明项目。一旦计数变为零,就没有更多项目,并且线程必须等待#3

count.store(number_of_items, memory_order_release)

输出(VS2015):

count.fetch_sub(1,memory_ order_acquire)
  

如果一个消费者线程,这很好; #include <atomic> #include <thread> #include <vector> #include <iostream> #include <mutex> std::vector<int> queue_data; std::atomic<int> count; std::mutex m; void process(int i) { std::lock_guard<std::mutex> lock(m); std::cout << "id " << std::this_thread::get_id() << ": " << i << std::endl; } void populate_queue() { unsigned const number_of_items = 20; queue_data.clear(); for (unsigned i = 0;i<number_of_items;++i) { queue_data.push_back(i); } count.store(number_of_items, std::memory_order_release); //#1 The initial store } void consume_queue_items() { while (true) { int item_index; if ((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0) //#2 An RMW operation { std::this_thread::sleep_for(std::chrono::milliseconds(500)); //#3 continue; } process(queue_data[item_index - 1]); //#4 Reading queue_data is safe } } int main() { std::thread a(populate_queue); std::thread b(consume_queue_items); std::thread c(consume_queue_items); a.join(); b.join(); c.join(); } 是一个读取,具有id 6836: 19 id 6836: 18 id 6836: 17 id 6836: 16 id 6836: 14 id 6836: 13 id 6836: 12 id 6836: 11 id 6836: 10 id 6836: 9 id 6836: 8 id 13740: 15 id 13740: 6 id 13740: 5 id 13740: 4 id 13740: 3 id 13740: 2 id 13740: 1 id 13740: 0 id 6836: 7 语义,并且存储具有fetch_sub()语义,因此存储与加载同步,并且线程可以从缓冲区读取项目。

     

如果两个线程读取,则第二个memory_order_acquire将看到第一个写入的值而不是商店写入的值。如果没有关于memory_order_release的规则,第二个线程就不会有第一个线程的fetch_sub(),除非第一个release sequence也是如此,否则读取共享缓冲区是不安全的有happens-before relationship语义,这会在两个消费者线程之间引入不必要的同步。如果fetch_sub()操作中没有memory_order_release规则或release sequence,那么就没有任何要求第二个消费者可以看到memory_order_release的商店,而您将拥有数据竞赛。

他的意思是什么?两个线程都应该看到fetch_sub的值是queue_data?但是在我的输出count中,线程中会逐渐减少。

  

值得庆幸的是,第一个20确实参与了发布序列,因此count与第二个fetch_sub()同步。两个消费者线程之间仍然没有同步关系。如图5.7所示。图5.7中的虚线显示了释放顺序,实线显示了store()   enter image description here

4 个答案:

答案 0 :(得分:7)

这意味着初始存储最终加载同步 - 即使最终加载读取的值不是直接存储在开头的相同值,但它是由修改的值可以参加的原子教学之一。一个更简单的例子,假设有三个线程竞赛执行这些指令(假设x在比赛前初始化为0)

// Thread 1:
A;
x.store(2, memory_order_release);

// Thread 2:
B;
int n = x.fetch_add(1, memory_order_relaxed);
C;

// Thread 3:
int m = x.load(memory_order_acquire);
D;

根据比赛的可能结果,为nm读取的可能值是多少?根据我们在A和{B上阅读的内容,我们对说明CDmn的排序提供了哪些保证? {1}}? 对于n,我们有两种情况,02。对于m,我们可以阅读0123。 这两者有六种有效组合。让我们看看每个案例:

  • m = 0, n = 0。我们没有 synchronize-with 关系,因此我们无法推断任何发生在之前的关系,除了明显的B 发生 - 在 C
  • 之前
  • m = 0, n = 2。即使fetch_add操作读取store写入的值,因为fetch_add具有relaxed内存排序,因此没有同步两条指令之间的关系。我们不能说A 发生在 C
  • 之前
  • m = 1, n = 0。与以前类似,由于fetch_add没有release语义,我们无法推断fetch_add与{{1}之间的同步与关系操作,因此我们不知道load 是否发生在 B
  • 之前
  • D。我们使用m = 2, n = 0语义acquire读取的值已使用load语义release编写。我们保证store store同步,因此load 发生在 A
  • D。与上面相同,m = 2, n = 2 与线程1中的store A load, hence D _happens-before_ fetch_add . As usual, the fact that the value read from store`d同步并不暗示任何同步关系。
  • is the same as the one。在这种情况下,m = 3, n = 2读取的数据由load写入,fetch_add读取的数据由fetch_add写入。但是,由于store具有fetch_add语义,因此relaxedstore之间以及fetch_addfetch_add之间不能进行同步。显然,在这种情况下,不能假设同步,与案例load相同。这里是发布序列概念派上用场的地方:线程1中的m = 0, n = 0语义release store同步只要正在读取的值已写入acquire,其中包含

    ,就可以在线程3中使用语义load
    1. 稍后在与发布操作相同的线程中执行的所有商店
    2. 从同一版本序列中读取值的所有原子读 - 修改 - 写操作。
    3. 在这种情况下,由于release sequence是一个原子读 - 修改 - 写操作,我们知道线程1 中的fetch_add与<{em>}中的store同步线程3,因此load 发生在 A之前。我们仍然无法说明DB的排序。

在你的情况下,你有这个pseoudocode,假设C

number_of_items = 2

假设读入// Thread 1 Item[0] = ...; Item[1] = ...; count.store(2,memory_order_release); // Thread 2 int i2 = 0; while (i2 = count.fetch_sub(1,memory_order_acquire) <= 0 ) sleep(); auto x2 = Item[i2-1]; process(x2); // Thread 3 int i3 = 0; while (i3 = count.fetch_sub(1,memory_order_acquire) <= 0 ) sleep(); auto x3 = Item[i3-1]; process(x3); 的第一个正值为i2,因此读入2的第一个正值为i3。由于从线程2读取的值已从线程1中的存储区写入,因此存储同步 - 并且我们知道线程1 中的1发生在之前线程2中的 Item[1] = ...;但是从线程3读取的值auto x2 = Item[1];已由线程2写入,其中1没有fetch_sub语义。因此,线程2中的release不会与线程3中的同步<{em} fetch_sub,但是因为线程2中的fetch_sub的一部分线程1中fetch_sub的发布链,线程1中的store与线程3中的store同步 - 知道fetch_sub发生在Item[0] = ...;

之前

答案 1 :(得分:3)

  

他的意思是什么?两个线程都应该看到count的值   20?但是在我的输出中,计数会在线程中逐渐减少。

不,他没有。对count的所有修改都是原子的,因此两个读者线程在给定代码中总会看到不同的值。

他正在讨论发布顺序规则的含义,即当给定线程执行release存储时,其他多个线程然后执行{{1}相同位置的加载形成发布序列,其中每个后续acquire加载与存储线程的发生 - 之前关系(即完成商店发生在加载之前)。这意味着读取器线程中的加载操作是与写入器线程的同步点,并且在存储器之前的写入器中的所有存储器操作必须完成并且在其相应的加载完成时在读取器中可见。

他说没有这个规则,只有第一个线程会因此与作者同步。因此,第二个线程在访问acquire时会有数据竞争(注意:不是 queue,无论如何都受到原子访问的保护。从理论上讲,只有在count上自己的加载操作之后,读者线程号2才能看到store count之前发生的数据的内存操作。发布顺序规则确保不会发生这种情况。

总结:发布顺序规则确保多个线程可以在单个存储上同步其负载。有问题的同步是对数据 other 的内存访问,而不是正在同步的实际原子变量(由于是原子的,因此保证同步)。

请注意这里添加:在大多数情况下,这些问题只是关于CPU架构的问题,这些问题在重新安排内存操作时很容易。英特尔架构不是其中之一:它是强烈排序的,并且只有少数非常具体的情况可以对内存操作进行重新排序。这些细微差别大多只与谈论其他架构有关,例如ARM和PowerPC。

答案 2 :(得分:1)

fetch_sub是读取 - 修改 - 写入操作。它以原子方式从内存地址读取值,通过提供的参数递减,然后将其写回内存地址。这一切都是以原子方式发生的。

现在,每个原子操作都直接读写内存地址。 CPU不依赖于寄存器或高速缓存行中的值来提高性能。它直接读取和写入内存地址,并防止其中的CPU在此时间内执行此操作。

什么“普通”(==放松)原子性不提供重新排序。编译器和CPU加密读写都是为了加快程序的执行速度。

看下面的例子:

atomic integer i
regular integer j

Thread A:
i <- 5
//do something else
i -> j
//make some decisions regarding j value.

Thread B:
i++

如果没有内存顺序,则允许编译器和CPU将代码转换为

Thread A:
i -> j
i <- 5
//do something else
//make some decisions regarding j value.

Thread B:
i++

当然不是我们想要的。决策是错误的。

我们需要的是内存重新排序。

内存顺序获取:不要在之前加扰内存访问 内存顺序释放:不要在

之后加扰内存访问

回到问题:

fetch_sub 读取值,写入值。通过指定memory order acquire我们说“我只关心发生的行动顺序之前阅读”
通过指定memory order release,我们说“我只关心在
写作之后发生的行动顺序。

但你之前和之后都关心内存访问!

如果您只有一个消费者线程,而sub_fetch不会影响任何人,因为生产者无论如何都使用普通storefetch_sub的影响仅对线程可见调用了fetch_sub。在这种情况下,您只关心阅读 - 阅读为您提供当前和更新的索引。

表示存储更新后的索引(比如x-1)后会发生什么。

但由于两个线程读取写入counter,因此线程A必须知道线程B 写入< / strong>计数器的新值,线程B知道线程A与读取计数器的值有关。反之亦然 - 线程B必须知道线程A 写了新值counter,线程A必须知道线程B即将来自柜台的价值

你需要两个保证 - 每个线程都声明它将同时读写共享计数器。您需要的内存顺序是std::memory_order_acquire_release

但这个例子很棘手。生产者线程只是在counter 中存储一个新值,而不管之前的值是否。如果生产者线程每次推送新项目时都要递增网络 - 你必须在生产者中使用std::memory_order_acquire_release消费者线程即使你有一个消费者

答案 3 :(得分:1)

我偶然发现了和你一样的问题。我认为我理解正确,然后他进入这个例子,只使用std :: memory_order_aquire。很难找到关于此的任何好消息,但最后我找到了一些有用的资料。 我不知道的主要信息是简单的事实,即读取 - 修改 - 写入操作总是在最新/最新值上工作,无论给出什么内存顺序(甚至std :: memory_order_relaxed)。这可以确保您在示例中不会有两次相同的索引。操作的顺序仍然会混淆(所以你不知道哪个fetch_sub会在另一个之前发生)。

这是安东尼·威廉姆斯自己的回答,说明读取 - 修改 - 写入操作始终适用于最新值:Concurrency: Atomic and volatile in C++11 memory model

另外,有人询问fetch_sub与shared_ptr引用计数的组合。这里的安东尼·威廉姆斯也做出了回应,并通过fetch_sub的重新排序使情况变得清晰: https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/OHv-oNSuJuk