前面我们研究了成对的释放获取同步关系(写 -> 读),本文我们了解一下在一个释放序列中的同步关系(写 -> 读改写 -> 读改写… -> 读)。
配合的内存次序,需要最先的写操作辅以 memory_order_release、memory_order_acq_rel 或 memory_order_seq_cst,而最后的读操作需辅以 memory_order_consume、memory_order_acquire 或 memory_order_seq_cst 标记,而中间的读改写操作,则可选择任意内存次序。
以下示例是一个简单的多线程的生产消费模式。
populateQueue() 判断缓冲区被读取的状况( count 是否小于 0 )来进行数据存储,comsomeQueueItems() 则根据 count 是否小于等于0,来判断是否要进行数据的读取。
程序通过 count.store(numberOfItems, std::memory_order_release) 的写操作,中间 if ((itemIndex = count.fetch_sub(1, std::memory_order_acquire)) <= 0) 的读改写操作,和最后的 if (count.load(std::memory_order_acquire) <= 0) 读操作,形成一个释放序列。
由于原子操作释放序列,不会产生重复消费同一数据,保证了一个不重复的逻辑,但不保证顺序一致, void process(int i) 与前面的读改写原子操作并不能形成一个原子操作。
#include
#include
#include
#include
#include
#include
std::vector<int> queueData;
std::atomic<int> count;
void populateQueue()
{
while (true)
{
if (count.load(std::memory_order_acquire) <= 0)
{
const unsigned numberOfItems = 20;
queueData.clear();
for (unsigned i = 0; i < numberOfItems; ++i)
{
queueData.push_back(i);
}
count.store(numberOfItems, std::memory_order_release);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
}
void waitForMoreItems()
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
void process(int i)
{
std::cout << i << std::endl;
}
void comsomeQueueItems()
{
while (true)
{
int itemIndex;
if ((itemIndex = count.fetch_sub(1, std::memory_order_acquire)) <= 0)
{
waitForMoreItems();
continue;
}
process(queueData[itemIndex - 1]);
}
}
auto main() -> int
{
std::thread a(populateQueue);
std::thread b(comsomeQueueItems);
std::thread c(comsomeQueueItems);
a.join();
b.join();
c.join();
return 0;
}
释放序列与同步关系,应该是原子操作的一个难点,比较难以理解。
如果想真正理解,恐怕要比较熟悉操作系统和现代 CPU 的一些硬知识。
多核 CPU 的多个内核,都有自己独立的寄存器和一级缓存,原子操作的内存次序,原则上是由操作系统如何处理何时将每个内核的缓存同步给系统内存决定的,我目前涉猎的知识不足以支撑将其原理完全阐明,如果有想深入理解原理的同学,需要自己扩展相应的知识储备。