使用std::memory_order和三个线程的同步谜语
Synchronization riddle with std::memory_order and three threads
这里有一个关于c++ 11中std::memory_order
规则的问题,当涉及到三个线程时。比如,一个线程生产者保存一个值并设置一个标志。然后,另一个线程中继在设置另一个标志之前等待这个标志。最后,第三个线程消费者等待来自中继的标志,这应该表明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
关键字"确保在释放相同原子变量的其他线程中的所有写操作在当前线程中都是可见的"。因此,当继电器设置flag2
时,data=42
对可见。它用memory_order_release
设置它,这"确保当前线程中的所有写操作在获得相同原子变量的其他线程中可见"。然而,data
没有被中继接触,所以消费者可能会以不同的顺序看到内存访问,并且当消费者看到flag2==True
时,data
可能未初始化。同样的参数也适用于
relay_2()
中更严格的内存排序。顺序一致的排序意味着"在所有标记为std::memory_order_seq_cst
的原子操作之间建立同步"。但是,这并没有说明变量data
。还是我理解错了,
relay_2()
就足够了?让我们通过访问
data
来解决relay_3()
中的情况。这里,data = data
行意味着在flag1
转到true
之后读取data
,并且在设置flag2
之前,中继线程写入data
。因此,消费者线程必须看到正确的值。然而,这个解决方案似乎有点奇怪。
data = data
行似乎是编译器(在顺序代码中)会立即优化出来的。假行在这里起作用了吗?用c++ 11
std::memory_order
特性实现跨三个线程的同步还有更好的方法吗?
顺便说一下,这不是一个学术问题。假设data
是一个大的数据块而不是单个整数,并且线程i需要将信息传递给(i+1)-该线程,该线程的数据已被索引≤i的所有线程处理。
在阅读了Michael Burr的回答后,很明显relay_1()
是足够的。请阅读他的帖子,以获得对这个问题的完全满意的解决方案。c++ 11标准提供了比单独从cppreference.com网站推断出的更严格的保证。因此,考虑迈克尔·伯尔的帖子中的论点是权威的,而不是我上面的评论。方法是在相关事件之间建立"线程间的happens-before"关系(这是可传递的)。
我认为relay_1()
足以通过data
将值42
从生产者传递给消费者。
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)。
我认为P
是U
的明显副作用,因为:
-
P
排在Q
之前,R
排在S
之前,T
排在U
之前(1.9/14) -
Q
同步R
,S
同步T
(29.3/2)
下面的所有点都被"线程间发生在"(1.10/11)的定义所支持:
Q -> S
因为标准说"线程间发生在评估B之前,如果…对于某些求值X, A与X同步,X在B之前排序"(Q
与R
同步,R
在S
之前排序,因此Q -> S
)S -> U
遵循类似的逻辑(S
与T
同步,T
在U
之前排序,因此S -> U
)Q -> U
,因为Q -> S
和S -> U
("一个线程间发生在评估B之前,如果…A线程间发生在X之前,X线程间发生在B之前")
最后,
-
P -> U
,因为P
在Q
和Q -> U
之前排序("线程间发生在评估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_acquire
和memory_order_release
对于实现更高级别的同步对象将不是很有用。
- 用于矢量处理的多个线程
- 如何在多个线程中创建 QSql数据库连接时防止名称冲突
- 如何声明由多个线程调用的 C++ DLL 的内部类,而无需导出类
- 在两个线程上读/写 64 位,无互斥/锁定/原子
- 将 10 个线程与原子布尔值同步
- C++线程安全:如果只有一个线程可以写入非原子变量,但多个线程从中读取. 会遇到问题吗?
- C++:在多个线程中访问同一数组/向量的不同单元格是否会产生数据竞赛?
- 一个线程等待多个线程事件
- 如果在 2 个线程中使用,是否值得将size_t声明为 std::atomic?
- 餐饮哲学家问题 - 只有 2 个线程工作
- 如果两个线程相互依赖,则 cpp 线程连接应使用连接导致死锁
- std::cout 来自多个线程
- 是否需要 mutex() 来安全地同时访问具有 2 个线程的数组的不同元素?
- 两个线程一个使用流 Api,另一个线程创建文件失败并出现错误ERROR_SHARING_VIOLATION
- 多个线程可以安全地同时将相同的值写入同一变量吗?
- 在 C Linux 中使用三个线程使用信号量同步按顺序打印 3 4 5 50 次
- 我无法让三个线程井井有条
- 在 c++ 中退出具有三个线程的两个并发队列
- 创建了三个线程来实例化同一个类.但很快只有一个线程继续运行
- 使用std::memory_order和三个线程的同步谜语