测试环形缓冲区实现的"cache not flushed to main memory"

testing "cache not flushed to main memory" for my ring-buffer implementation

本文关键字:flushed not to main memory cache 测试 缓冲区 实现      更新时间:2023-10-16

upd以反映最近的建议,使curWriteNum不稳定,用std::cout重新排列pool.Commit();

我创建了一个读写器环缓冲区(示例中的类ArrayPool)。我喜欢它,但我担心当一个(读取器)线程因为另一个线程在另一个CPU上运行并使用另一个缓存或类似的东西而没有看到新值时,我会遇到问题。

我已经创建了测试程序。它创建了100个线程,所以我认为它们必须或多或少地分布在所有可用的处理器上。

#include <stdint.h>
#include <iostream>
#include <boost/thread.hpp>
#include <chrono>
#include <thread>
template<class T> class ArrayPool
{
public:
ArrayPool() {
};
~ArrayPool(void) {
};
bool IsEmpty() {
return curReadNum == curWriteNum;
}
T* TryGet()
{
if (curReadNum == curWriteNum)
{
return NULL;
}
T* result = &storage[curReadNum & MASK];
++curReadNum;
return result;
}
T* Obtain() {
return &storage[curWriteNum & MASK];
}
void Commit()
{
++curWriteNum;
if (curWriteNum - curReadNum > length)
{
std::cout <<
"ArrayPool curWriteNum - curReadNum > length! " <<
curWriteNum << " - " << curReadNum << " > " << length << std::endl;
}
}
private:
static const uint32_t length = 65536;
static const uint32_t MASK = length - 1;
T storage[length];
volatile uint32_t curWriteNum;
uint32_t curReadNum;
};
struct myStruct {
int value;
};
ArrayPool<myStruct> pool;
void ReadThread() {
myStruct* entry;
while(true) {
while ((entry = pool.TryGet()) != NULL) {
std::cout << entry->value << std::endl;
}
}
}
void WriteThread(int id) {
std::chrono::milliseconds dura(1000 * id);
std::this_thread::sleep_for(dura);
myStruct* storage = pool.Obtain();
storage->value = id;
pool.Commit();
std::cout << "Commited value! " << id << std::endl;
}

int main( void )
{
boost::thread readThread = boost::thread(&ReadThread);
boost::thread writeThread;
for (int i = 0; i < 100; i++) {
writeThread = boost::thread(&WriteThread, i);
}
writeThread.join();
return 0;
}

我试着在2*Xeon E5服务器上运行这个程序,一切都很好,每个值都被捕获了:

...
Commited value! 19
19
Commited value! 20
20
Commited value! 21
21
Commited value! 22
22
Commited value! 23
23
Commited value! 24
24
Commited value! 25
25
Commited value! 26
26
....

同样在Process Explorer中,我可以看到线程数是如何从~101减少到1的。这是否意味着我的ArrayPool类很好,在现代英特尔处理器上不可能面临任何此类问题?如果有可能重现"缓存"的问题,那么该怎么做呢?

首先,不,这个程序是不安全的。您可能确实无法在特定编译器和体系结构组合上重现缓存排序问题。特别要注意的是,这不仅仅与处理器缓存有关。理论上,编译器可以交换赋值操作。那么,你能做些什么来增加风险呢?

  • 在不同(通常是高)优化级别上尝试不同的编译器。例如,如果我使用x86_64-linux-gnu-g++-4.8 -O3进行编译,则读取器会错过所有值。编译器很可能会内联TryGet,并注意到循环体不会影响条件。因此,它可以将条件的结果缓存在寄存器值中。为了避免这种行为,您需要将条件中的一个变量标记为volatile
  • 尝试不同的处理器体系结构(针对不同的缓存行为)
  • Commit和实际值write之间,有一个系统调用正在进行。这需要相当长的时间。交换这些操作

即使您想不锁定,也需要最低限度的读取和写入障碍。看看这篇LWN文章,了解它的复杂性,并学习一个库,它将帮助您编写无锁算法。