gpt4 book ai didi

c++ - std::memory_order 和三个线程的同步谜题

转载 作者:塔克拉玛干 更新时间:2023-11-03 00:35:30 25 4
gpt4 key购买 nike

这是一个关于 std::memory_order 的问题C++11 中的规则,当涉及到三个线程时。比如说,一个线程 producer 保存一个值并设置一个标志。然后,另一个线程 relay 在设置另一个标志之前等待这个标志。最后,第三个线程 consumer 等待来自 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 时对 relay 可见.它设置为 memory_order_release ,它“确保当前线程中的所有写入在获取相同原子变量的其他线程中可见”。然而,data没有被relay触及,所以消费者可能会看到不同顺序的内存访问,data消费者看到flag2==True时可能未初始化.

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

    还是我理解错了这里和relay_2()够了吗?

  3. 让我们通过访问 data 来解决这个问题如 relay_3() .在这里,行 data = data暗示dataflag1 之后阅读去了true , relay 线程写入 data , 在设置 flag2 之前.因此,消费者线程必须看到正确的值。

    但是,这个解决方案似乎有点奇怪。线路data = data似乎是编译器会(在顺序代码中)立即优化掉的东西。

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

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

编辑:

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

最佳答案

我认为 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 表示“线程间发生在 B 之前”(C++11 1.10/11)。

我认为 PU 的可见副作用,因为:

  • P排在Q之前,R排在S之前,T 排在 U (1.9/14)
  • 之前
  • QR同步,ST同步 (29.3/2)

“线程间发生在之前”(1.10/11) 的定义支持以下所有要点:

  • Q -> S 因为标准说“线程间发生在评估 B 之前,如果......对于某些评估 X,A 与 X 同步并且 X 在 B 之前排序"(QR 同步,R 排在 S 之前,所以 Q -> S)

  • S -> U 遵循类似的逻辑(ST 同步并且 T 被排序在 U 之前,所以 S -> U)

  • Q -> U 因为 Q -> SS -> U(“线程间发生在评估 B if ... A 线程间发生在 X 之前,X 线程间发生在 B 之前)

最后,

  • P -> U 因为 PQQ -> U 之前排序(“A inter -线程发生在评估 B 之前,如果 ... A 在 X 之前排序,并且 X 线程间发生在 B 之前")

由于 P 线程间发生在 U 之前,P 发生在 U 之前 (1.10/12) 并且P 是相对于 U (1.10/13) 的“可见副作用”。

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

对于这个生产者/消费者问题,relay_2() 至少和 relay_1() 一样好,因为在存储操作中 memory_order_seq_cst 是一个释放,在加载操作中 memory_order_seq_cst 是一个获取(参见 29.3/1)。所以可以遵循完全相同的逻辑。使用 memory_order_seq_cst 的操作有一些额外的属性,这些属性与所有 memory_order_seq_cst 在其他 memory_order_seq_cst 操作中如何排序有关,但这些属性不会出现玩这个例子。

我认为如果没有像这样的传递行为,memory_order_acquirememory_order_release 将不会对实现更高级别的同步对象非常有用。

关于c++ - std::memory_order 和三个线程的同步谜题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16115014/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com