同步谜语std :: memory_order和三个线程

时间:2013-04-19 23:49:02

标签: c++ c++11 atomic

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

这是一个最小程序,采用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()不足,可以触发消费者中的assert。根据上面引用的C ++参考,memory_order_acquire关键字“确保在当前线程中可以看到释放相同原子变量的其他线程中的所有写入”。因此,data=42在设置flag2时对中继可见。它使用memory_order_release设置它,“确保当前线程中的所有写入在获取相同原子变量的其他线程中可见”。但是,中继未触及data,因此消费者可能会以不同的顺序看到内存访问,data可能在未初始化时未初始化消费者会看到flag2==True

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

    或者我在这里理解错误并relay_2()就足够了吗?

  3. 让我们通过data访问relay_3()来解决问题。在这里,行data = data表示data转到flag1后会读取true,而中继线程会写入data ,在设置flag2之前。因此,消费者线程必须看到正确的值。

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

    虚拟线在这里有效吗?使用C ++ 11 std::memory_order功能实现三个线程同步的更好方法是什么?

  4. 顺便说一下,这不是一个学术问题。想象一下,data是一个大数据块而不是一个整数,而 i -th线程需要将信息传递给( i + 1) - 索引≤ i 的所有线程处理数据的元素。

    编辑:

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

1 个答案:

答案 0 :(得分:4)

我认为relay_1()足以通过42将值data从生产者传递给消费者。

为了表明这一点,首先我会给感兴趣的操作提供单字母名称:

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

我将使用符号A -> B来表示&#34;一个内部线程发生在B&#34之前; (C ++ 11 1.10 / 11)。

我认为P对于U是一个明显的副作用,因为:

  • PQ之前排序,RS之前排序,TU之前排序(1.9 / 14)
  • QR同步,ST同步(29.3 / 2)

&#34的定义支持所有接下来的点;线程发生在&#34;之前。 (1.10 / 11):

  • Q -> S,因为标准说&#34;如果...对于某些评估X,则A线程在评估B之前发生,A与X同步,并且在B&#34之前对X进行排序。 (QR同步,RS之前排序,因此Q -> S

  • S -> U遵循类似逻辑(ST同步,TU之前排序,因此S -> U)< / p>

  • Q -> U因为Q -> SS -> U(&#34;在线程发生在评估B之前,如果......线程发生在X和X之前线程间发生在B&#34;)

  • 之前

最后,

  • P -> U因为PQQ -> U之前排序(&#34;如果...之前对A进行排序,则会在评估B之前发生线程间X和X线程发生在B&#34;)
  • 之前

由于P线程发生在U之前,P发生在U之前(1.10 / 12),P是&#34;可见副作用&#34;关于U(1.10 / 13)。

relay_3()也足够了,因为data=data表达式无关紧要。

出于此生产者/消费者问题的目的,relay_2()至少与relay_1()一样好,因为在商店操作中memory_order_seq_cst是一个版本并且在加载操作memory_order_seq_cstmemory_order_seq_cst 1}}是一种获得(见29.3 / 1)。因此可以遵循完全相同的逻辑。使用memory_order_seq_cst的操作具有一些其他属性,这些属性与所有memory_order_seq_cst在其他memory_order_acquire操作中的排序方式有关,但这些属性在此示例中并未发挥作用。

如果没有像这样的传递行为,我认为memory_order_release和{{1}}对于实现更高级别的同步对象不会非常有用。