当涉及到三个线程时,这是关于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();
}
第一个功能relay_1()
不足,可以触发消费者中的assert
。根据上面引用的C ++参考,memory_order_acquire
关键字“确保在当前线程中可以看到释放相同原子变量的其他线程中的所有写入”。因此,data=42
在设置flag2
时对中继可见。它使用memory_order_release
设置它,“确保当前线程中的所有写入在获取相同原子变量的其他线程中可见”。但是,中继未触及data
,因此消费者可能会以不同的顺序看到内存访问,data
可能在未初始化时未初始化消费者会看到flag2==True
。
同样的论点适用于relay_2()
中更严格的记忆顺序。顺序一致的排序意味着“在标记为std::memory_order_seq_cst
的所有原子操作之间建立同步”。但是,这并没有说明变量data
。
或者我在这里理解错误并relay_2()
就足够了吗?
让我们通过data
访问relay_3()
来解决问题。在这里,行data = data
表示data
转到flag1
后会读取true
,而中继线程会写入data
,在设置flag2
之前。因此,消费者线程必须看到正确的值。
然而,这个解决方案似乎有点奇怪。行data = data
似乎是编译器(在顺序代码中)立即优化的行。
虚拟线在这里有效吗?使用C ++ 11 std::memory_order
功能实现三个线程同步的更好方法是什么?
顺便说一下,这不是一个学术问题。想象一下,data
是一个大数据块而不是一个整数,而 i -th线程需要将信息传递给( i + 1) - 索引≤ i 的所有线程处理数据的元素。
在阅读了Michael Burr的回答之后,很明显relay_1()
就足够了。请阅读他的帖子,以获得完全令人满意的问题解决方案。 C ++ 11标准提供了比单独从cppreference.com网站推断出的更严格的保证。因此,考虑迈克尔伯尔的帖子中的论证是权威的,而不是我上面的评论。要走的路是在相关事件之间建立一个“线程间发生 - 关系”(它是传递性的)。
答案 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
是一个明显的副作用,因为:
P
在Q
之前排序,R
在S
之前排序,T
在U
之前排序(1.9 / 14) Q
与R
同步,S
与T
同步(29.3 / 2)&#34的定义支持所有接下来的点;线程发生在&#34;之前。 (1.10 / 11):
Q -> S
,因为标准说&#34;如果...对于某些评估X,则A线程在评估B之前发生,A与X同步,并且在B&#34之前对X进行排序。 (Q
与R
同步,R
在S
之前排序,因此Q -> S
)
S -> U
遵循类似逻辑(S
与T
同步,T
在U
之前排序,因此S -> U
)< / p>
Q -> U
因为Q -> S
和S -> U
(&#34;在线程发生在评估B之前,如果......线程发生在X和X之前线程间发生在B&#34;)
最后,
P -> U
因为P
在Q
和Q -> 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_cst
中memory_order_seq_cst
1}}是一种获得(见29.3 / 1)。因此可以遵循完全相同的逻辑。使用memory_order_seq_cst
的操作具有一些其他属性,这些属性与所有memory_order_seq_cst
在其他memory_order_acquire
操作中的排序方式有关,但这些属性在此示例中并未发挥作用。
如果没有像这样的传递行为,我认为memory_order_release
和{{1}}对于实现更高级别的同步对象不会非常有用。