在C ++ 11中是否有任何方法可以为对象实现无锁缓存,从多个线程访问是安全的?我想要缓存的计算并不是非常便宜,但也不是非常昂贵,因此在我的情况下需要锁定会破坏缓存的目的。 IIUC,std::atomic
并不保证无锁。
编辑:由于计算费用并不昂贵,我实际上并不介意它运行一次或两次太多。但我 - 需要确保所有消费者都能获得正确的价值。在下面的天真示例中,这是不能保证的,因为由于内存重新排序,线程可能会获得未初始化的m_val
值,因为另一个线程集m_alreadyCalculated
是的,但还没有设定m_val
的价值。
Edit2:下面的评论指出,对于基本类型,std::atomic
可能是无锁的。如果是,在下面的示例中使用C ++ 11的内存排序的正确方法是什么,以确保将m_alreadyCalculated
设置为true是不可能的在m_val
的值设置之前?
非线程安全缓存示例:
class C {
public:
C(int param) : m_param(param) {}
getValue() {
if (!m_alreadyCalculated) {
m_val = calculate(m_param);
m_alreadyCalculated = true;
}
return m_val;
}
double calculate(int param) {
// Some calculation
}
private:
int m_param;
double m_val;
bool m_alreadyCalculated = false;
}
答案 0 :(得分:2)
考虑一下:
class C {
public:
double getValue() {
if (alreadyCalculated == true)
return m_val;
bool expected = false;
if (calculationInProgress.compare_exchange_strong(expected, true)) {
m_val = calculate(m_param);
alreadyCalculated = true;
// calculationInProgress = false;
}
else {
// while (calculationInProgress == true)
while (alreadyCalculated == false)
; // spin
}
return m_val;
}
private:
double m_val;
std::atomic<bool> alreadyCalculated {false};
std::atomic<bool> calculationInProgress {false};
};
事实上它并不是无锁的,里面有旋转锁。但是,如果您不想通过多个线程运行calculate()
,我认为您无法避免这种锁定。
getValue()
在这里变得更加复杂,但重要的是,计算m_val
后,它将始终在第一个if
语句中立即返回。
<强>更新强>
出于性能原因,将整个类填充到缓存行大小也是一个好主意。
更新2
原始答案中有一个错误,感谢JVApen指出这一点(标记为注释)。变量calculationInProgress
最好重命名为calculationHasStarted
。
另请注意,此解决方案假定calculate()
不会引发异常。
答案 1 :(得分:0)
std :: atomic不保证是无锁的,但您可以查看std::atomic<T>::is_lock_free()
或std::atomic::is_always_lock_free()
以查看您的实现是否可以执行此锁定。
另一种方法可能是使用std::call_once
,但是根据我的理解,这更糟糕,因为它意味着阻止其他线程。
因此,在这种情况下,我会使用std :: atomic来同时使用m_val和alreadyCalculated。其中包含2个(或更多)线程计算相同结果的风险。
答案 2 :(得分:0)
这里只回答一个技术问题:要确保在标志之前更新值,请使用发布语义更新标志。释放语义的含义是这个更新必须(被视为)在所有先前的更新之后发生。在x86上,它只表示更新前的编译器屏障,并对内存进行更新,而不是注册,如下所示:
asm volatile("":::"memory");
*(volatile bool*)&m_alreadyCalculated = true;
这正是原子集在发布语义中所做的事情