一、抛出问题
以下代码是否会出现竞争问题?
#include <atomic>
#include <thread>
std::vector<int> queue_data;
std::atomic<int> count;
void populate_queue()
{
unsigned const number_of_items=20;
queue_data.clear();
for(unsigned i=0;i<number_of_items;++i)
{
queue_data.push_back(i);
}
count.store(number_of_items,std::memory_order_release);//初始化存储
}
void consume_queue_items()
{
while(true)
{
int item_index;
if((item_index=count.fetch_sub(1,std::memory_order_acquire))<=0)//一个读-改-写操作
{
wait_for_more_items();
continue;
}
process(queue_data[item_index-1]);
}
}
int main()
{
std::thread a(populate_queue);
std::thread b(consume_queue_items);
std::thread c(consume_queue_items);
a.join();
b.join();
c.join();
}
二、C++ 释放序列
1、定义
- C++标准定义
在原子对象 M 上进行的释放操作 A 之后,由下列内容组成的 M 修改顺序的最长相接子序列
- 与进行 A 的同一线程所进行的写入(C++20 前)
- 任何线程对于 M 的原子的读修改写操作
被称为 A 所引领的释放序列。
- 通俗的说:当存储操作被标记为memory_order_release, memory_order_acq_rel或memory_order_seq_cst, 加载被标记为memory_order_consum, memory_order_acquire或memory_order_sqy_cst, 并且操作链上的每一加载操作都会读取之前操作写入的值, 因此链
上的操作构成了一个释放序列(release sequence), 并且初始化存储同步(对应memory_order_acquire或memory_order_seq_cst)或是前序依赖(对应memory_order_consume)的最终加载。 操作链上的任何原子“读-改-写”操作可以拥有任意个存储序列(甚至是memory_order_relaxed)。
2、上面例子分析
- 可能的问题
当有两个读取线程时, 第二个fetch_sub()操作将看到被第一个线程修改的值, 且没有值通过store写入其中。 先不管释放序列的规则, 这里第二个线程与第一个线程不存在先行关系, 并且其对共享缓存中值的读取也不安全, 除非第一个fetch_sub()是带有memory_order_release语义的, 这个语义为两个消费者线程间建立了不必要的同步。 无论是释放序列的规则, 还是带有memory_order_release语义的fetch_sub操作, 第二个消费者看到的是一个空的queue_data, 无法从其获取任何数据, 并且这里还会产生条件竞争。 - 分析结果
幸运的是, 第一个fetch_sub()对释放顺序做了一些事情, 所以store()能同步与第二个fetch_sub()操作。 这里, 两个消费者线程间不需要同步关系。 这个过程在下图中展示, 其中虚线表示的就是释放顺序, 实线表示的是先行关系.
操作链中可以有任意数量的链接, 但是提供的都是“读-改-写”操作, 比如fetch_sub(),则store() 仍将与每一个使用memory_order_acquire语义的操作进行同步。 在这里例子中, 所有链接都是一样的, 并且都是获取操作, 但它们可由不同内存序列语义组成的操作混合。(也就是不是单纯的获取操作) - 结论
有多个线程, 即使有(有序的)多个“读-改-写”操作(所有操作都已经做了适当的标记)在存储和加载操作之间, 依旧可以获取原子变量存储与加载的同步关系。