更新以反映最简单的建议curWriteNum
易变,重新排列pool.Commit();
std::cout
我创建了一个读写器一个写入器环缓冲区(示例中为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类很好,在现代英特尔处理器上不可能遇到任何此类问题?如果可以重现“缓存”问题那么该如何做?
答案 0 :(得分:1)
首先,没有这个程序不安全。您可能无法在特定的编译器和体系结构组合上重现缓存排序问题。特别要注意的是,这不仅仅与您的处理器缓存有关。理论上,您的编译器可以交换赋值操作。那么你可以做些什么来增加风险?
x86_64-linux-gnu-g++-4.8 -O3
进行编译,则读者会错过所有值。编译器很可能会内联TryGet
,并注意到循环体不会影响条件。因此,它可以将条件的结果缓存在寄存器值中。要避免此行为,您需要将条件中的一个变量标记为volatile
。Commit
和实际值写入之间,系统调用正在进行中。这需要相当长的时间。交换这些操作。即使你想要没有锁定,你也需要在最低限度的阅读障碍和写障碍。查看this LWN article以了解复杂性并学习可帮助您编写无锁算法的库。