6

std::memory_order当涉及到三个线程时,这里有一个关于 C++11 中规则的问题。比如说,一个线程生产者保存了一个值并设置了一个标志。然后,另一个线程中继在设置另一个标志之前等待这个标志。最后,第三个线程consumer等待来自relay的标志,这应该表示data已为consumer准备好。

这是一个最小程序,采用 C++ 参考 ( http://en.cppreference.com/w/cpp/atomic/memory_order ) 中的示例样式:

#include <thread>
#include <atomic>
#include <cassert>

std::atomic<bool> flag1 = ATOMIC_VAR_INIT(false);
std::atomic<bool> flag2 = ATOMIC_VAR_INIT(false);
int data;

void producer()
{
  data = 42;
  flag1.store(true, std::memory_order_release);
}

void relay_1()
{
  while (!flag1.load(std::memory_order_acquire))
    ;
  flag2.store(true, std::memory_order_release);
}

void relay_2()
{
  while (!flag1.load(std::memory_order_seq_cst))
    ;
  flag2.store(true, std::memory_order_seq_cst);
}

void relay_3()
{
  while (!flag1.load(std::memory_order_acquire))
    ;
  // Does the following line make a difference?
  data = data;
  flag2.store(true, std::memory_order_release);
}

void consumer()
{
  while (!flag2.load(std::memory_order_acquire))
    ;
  assert(data==42);
}

int main()
{
  std::thread a(producer);
  std::thread b(relay_1);
  std::thread c(consumer);
  a.join(); b.join(); c.join();
}

注释:

  1. 第一个功能relay_1()不足,可以触发assertin consumer。根据上面引用的 C++ 参考,memory_order_acquire关键字“确保在其他线程中释放相同原子变量的所有写入在当前线程中都是可见的”。因此,设置 时对中继data=42是可见的。它将它设置为“确保当前线程中的所有写入在获取相同原子变量的其他线程中可见”。但是,由于没有被relay触及,因此消费者可能会看到内存访问顺序不同,并且在消费者看到时可能未初始化。flag2memory_order_releasedatadataflag2==True

  2. 相同的论点适用于relay_2(). 顺序一致的排序意味着“在所有标记为 的原子操作之间建立了同步std::memory_order_seq_cst”。但是,这并没有说明变量data

    还是我在这里理解错误并且relay_2()足够了?

  3. 让我们通过访问来解决这种data情况relay_3()。在这里,该行data = data表示在go todata之后读取,中继线程在设置之前写入, 。因此,消费者线程必须看到正确的值。flag1truedataflag2

    然而,这个解决方案似乎有点奇怪。该行data = data似乎编译器会(在顺序代码中)立即优化。

    虚拟线在这里起作用吗?std::memory_order使用 C++11特性实现跨三个线程同步的更好方法是什么?

顺便说一句,这不是一个学术问题。想象这data是一个大数据块而不是单个整数,第i个线程需要将信息传递给第 ( i +1) 个线程,直到数据已被所有具有索引的线程处理的元素≤<em>i。

编辑:

阅读迈克尔伯尔的回答后,很明显这就relay_1()足够了。请阅读他的帖子以获得完全令人满意的问题解决方案。C++11 标准提供了比仅从 cppreference.com 网站推断出的更严格的保证。因此,请认为 Michael Burr 帖子中的论点是权威的,而不是我上面的评论。要走的路是在所讨论的事件之间建立一个“线程间发生之前”的关系(它是可传递的)。

4

1 回答 1

5

I think that relay_1() is sufficient to pass the value 42 from the producer to the consumer through data.

To show this, first I'll give single-letter names to the operations of interest:

void producer()
{
    /* P */ data = 42;
    /* Q */ flag1.store(true, std::memory_order_release);
}

void relay_1()
{
  while (/* R */ !flag1.load(std::memory_order_acquire))
    ;

  /* S */ flag2.store(true, std::memory_order_release);
}


void consumer()
{
  while (/* T */ !flag2.load(std::memory_order_acquire))
    ;
  /* U */ assert(data==42);
}

I'm going to use the notation A -> B to mean that "A inter-thread happens before B" (C++11 1.10/11).

I argue that P is a visible side-effect with respect to U because:

  • P is sequenced before Q, R is sequenced before S, and T is sequenced before U (1.9/14)
  • Q synchronizes with R, and S synchronizes with T (29.3/2)

All of the next points are supported by the definition of "inter-thread happens before" (1.10/11):

  • Q -> S since the standard says "A inter-thread happens before an evaluation B if ... for some evaluation X, A synchronizes with X and X is sequenced before B" (Q synchronizes with R and R is sequenced before S, so Q -> S)

  • S -> U following similar logic (S synchronizes with T and T is sequenced before U, so S -> U)

  • Q -> U because Q -> S and S -> U ("A inter-thread happens before an evaluation B if ... A inter-thread happens before X and X inter-thread happens before B")

And finally,

  • P -> U because P is sequenced before Q and Q -> U ("A inter-thread happens before an evaluation B if ... A is sequenced before X and X inter-thread happens before B")

Since P inter-thread happens before U, P happens before U (1.10/12) and P is a "visible side-effect" with respect to U (1.10/13).

relay_3() is also sufficient because the data=data expression is irrelevant.

And for the purpose of this producer/consumer problem, relay_2() is at least as good as relay_1() because in a store operation memory_order_seq_cst is a release and in a load operation memory_order_seq_cst is an acquire (See 29.3/1). So the exact same logic can be followed. Operations using memory_order_seq_cst have some additional properties having to do with how all memory_order_seq_cst are sequenced among other memory_order_seq_cst operations, but those properties don't come into play in this example.

I think that memory_order_acquire and memory_order_release would not be very useful for implementing higher level synchronization objects if there weren't a transitive behavior such as this.

于 2013-04-20T09:39:29.660 回答