开源项目readerwriterqueue学习
开源项目readerwriterqueue学习
前置知识
内存屏障-Memory Barrier
内存屏障(Memory Barrier)是什么?
内存屏障是一种CPU指令,用于控制内存操作的顺序。它能保证在多线程或多核环境中,某些内存读写操作按照预期的顺序执行,以防止编译器或 CPU 重排序 导致的数据不一致问题。
为什么需要内存屏障?
- 编译器优化: 编译器可能会为了提高性能,重新排序代码中的读写操作顺序。
- CPU 重排序: 现代 CPU 为了提高指令流水线的效率,也会对指令进行重排序执行。
- 多核缓存一致性: 在多核 CPU 中,每个核心有自己的缓存,如果不加以控制,不同核心之间对共享内存的访问可能出现不一致的情况。
例如,在下面这段代码中:
// 线程 1:
x = 1;
flag = 1;
// 线程 2:
if (flag == 1) {
print(x);
}
可能出现的重排序:
- 编译器可能会把
x = 1;
和flag = 1;
顺序对调。 - CPU 可能先把
flag = 1;
写入内存,再把x = 1;
写入。 - 这会导致线程 2 中
flag
为 1,但x
还是初始值,出现未定义行为。
内存屏障的作用:
内存屏障通过禁止重排序,确保某些指令之前的内存操作完成后,才能执行后续的指令。
- 写内存屏障(Store Memory Barrier,
StoreStore
): 确保屏障之前的所有写操作在内存中完成后,才能进行屏障之后的写操作。 - 读内存屏障(Load Memory Barrier,
LoadLoad
): 确保屏障之前的所有读操作完成后,才能进行屏障之后的读操作。 - 全内存屏障(Full Memory Barrier,
FullFence
): 既保证读操作也保证写操作的顺序。
在 C++ 中的实现:
C++11 提供了 std::atomic_thread_fence
来显式地插入内存屏障,例如:
#include <atomic>
int x = 0;
int flag = 0;
void thread1() {
x = 1;
std::atomic_thread_fence(std::memory_order_release); // 写内存屏障
flag = 1;
}
void thread2() {
if (flag == 1) {
std::atomic_thread_fence(std::memory_order_acquire); // 读内存屏障
// 确保读取 flag == 1 时,x 一定已被写入 1
std::cout << x << std::endl;
}
}
std::memory_order_release
确保x = 1
在flag = 1
之前完成。std::memory_order_acquire
确保flag = 1
被读取后,再读取x
时一定是最新值。
在 x86 架构上的特点:
- x86 CPU 上,写操作不会重排序,读操作可能重排序。
- 因此,x86 上的写内存屏障通常会被优化为无操作(No-Op)。
- 读内存屏障 会被编译成
LFENCE
指令,确保读操作顺序。 - 全内存屏障 则被编译成
MFENCE
指令,强制读写顺序。
总结:
- 内存屏障 是一种CPU指令,用于防止重排序,保证多线程环境下的内存可见性和一致性。
- 在 C++ 中,
std::atomic_thread_fence
可以显式插入内存屏障。 - 在 x86 架构上,写内存屏障通常是No-Op,读内存屏障为
LFENCE
,全内存屏障为MFENCE
。
获取队列元素个数
在README
里的这段示例代码当中:
BlockingReaderWriterQueue<int> q;
std::thread reader([&]() {
int item;
#if 1
for (int i = 0; i != 100; ++i) {
// Fully-blocking:
q.wait_dequeue(item);
}
#else
for (int i = 0; i != 100; ) {
// Blocking with timeout
if (q.wait_dequeue_timed(item, std::chrono::milliseconds(5)))
++i;
}
#endif
});
std::thread writer([&]() {
for (int i = 0; i != 100; ++i) {
q.enqueue(i);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
});
writer.join();
reader.join();
assert(q.size_approx() == 0);
这里的approx
指的是approximate,解释如下:
approx
是 approximate 的缩写,意思是近似的。
在这行代码中:
assert(q.size_approx() == 0);
size_approx()
的意思是 “近似大小”。
- 它返回的是队列中元素的近似数量,而不是精确值。
- 这是因为无锁队列通常在高并发场景下使用,为了保证性能,不会加锁精确计算大小,而是给出一个大致的估计值。
- 例如,在某些线程正在
enqueue()
或dequeue()
时,size_approx()
可能返回一个略微偏大的或偏小的值。
这有助于提高性能,但可能不完全准确。
如果需要精确值,则需要额外的同步机制,但会影响性能。