C++ 锁护板的有效位置-从有效现代C和x2B的第16项开始+;
在第16项:“使const成员函数线程安全”中,有一个代码如下:C++ 锁护板的有效位置-从有效现代C和x2B的第16项开始+;,c++,multithreading,atomic,C++,Multithreading,Atomic,在第16项:“使const成员函数线程安全”中,有一个代码如下: class Widget { public: int magicValue() const { std::lock_guard<std::mutex> guard(m); // lock m if (cacheValid) return cachedValue; else { auto val1 = expensiveComputation1();
class Widget {
public:
int magicValue() const
{
std::lock_guard<std::mutex> guard(m); // lock m
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
} // unlock m
private:
mutable std::mutex m;
mutable int cachedValue; // no longer atomic
mutable bool cacheValid{ false }; // no longer atomic
};
类小部件{
公众:
int magicValue()常量
{
std::lock_guard(m);//lock m
if(cacheValid)返回cachedValue;
否则{
auto val1=费用计算1();
auto val2=费用计算2();
cachedValue=val1+val2;
cacheValid=true;
返回cachedValue;
}
}//解锁m
私人:
可变std::mutexm;
可变int cachedValue;//不再是原子的
可变布尔缓存有效{false};//不再是原子的
};
我想知道为什么每次调用magicValue()时都要执行std::lock\u-guard,下面的工作不符合预期吗
class Widget {
public:
int magicValue() const
{
if (cacheValid) return cachedValue;
else {
std::lock_guard<std::mutex> guard(m); // lock m
if (cacheValid) return cachedValue;
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
} // unlock m
private:
mutable std::atomic<bool> cacheValid{false};
mutable std::mutex m;
mutable int cachedValue; // no longer atomic
};
类小部件{
公众:
int magicValue()常量
{
if(cacheValid)返回cachedValue;
否则{
std::lock_guard(m);//lock m
if(cacheValid)返回cachedValue;
auto val1=费用计算1();
auto val2=费用计算2();
cachedValue=val1+val2;
cacheValid=true;
返回cachedValue;
}
}//解锁m
私人:
可变std::原子缓存有效{false};
可变std::mutexm;
可变int cachedValue;//不再是原子的
};
这样,所需的互斥锁就更少了,代码效率也更高。这里我假设atomica总是比互斥体快
[编辑]
对于完整性,我测量了两种方法的效率,第二种方法看起来只快了6%。我实际上认为您的代码片段单独来说是正确的,但它依赖于一个在现实世界中通常不正确的假设:它假设
cacheValid
从假变为真,但决不能使之倒退,那就是变得无效
在旧代码中,mutex
保护cachedValue
上的所有读写操作。在您的新代码中,实际上在互斥锁之外有一个cachedValue
的读取访问。这意味着一个线程可以读取该值,而另一个线程正在写入该值。问题是,只有当cacheValid
为true时,才会发生互斥锁外部的读取。但是如果cacheValid
为true,则不会发生写入cacheValid
只能在所有写入完成后变为true(请注意,这是强制的,因为cacheValid
上的赋值运算符将使用最严格的内存排序保证,因此不能使用块中的早期指令对其重新排序)
但是,假设编写了另一段可以使缓存无效的代码:Widget::invalidateCache()
。这段代码只会再次将cacheValid
设置为false。在旧代码中,如果从不同线程重复调用invalidateCache
和magicValue
,则后一个函数可能会在任何给定点重新计算值,也可能不会重新计算值。但即使您的复杂计算在每次调用时都返回不同的值(因为它们使用全局状态),您也总是会得到旧值或新值,而不会得到其他值。但是现在考虑以下代码中的执行顺序:
magicValue
,并检查cacheValid
的值。这是真的。它在继续之前就被打断了invalidateCache
,然后立即调用magicValue
magicValue
发现缓存无效,获取互斥锁,开始计算,并开始写入cacheValid
cacheValid
int
通常是32位的,而通常32位的写入和读取是原子的。因此,实际上不可能散布或“撕裂”cachedValue
的值。但在不同的体系结构上,或者如果您使用的是integer以外的类型(例如,超过64位的任何类型),则不能保证写入或读取是原子的。因此,作为对magicValue
的返回,您可以得到一些既不是旧值也不是新值的东西,而是一些奇怪的按位混合,甚至不是有效对象
很高兴你能找到这个。我想,为了简化示例,作者忘记了不再需要严格地将互斥锁放在外部。您的第二个代码片段显示了双重检查锁定模式(DCLP)的一个完全有效的实现,并且(可能)比Meyers的解决方案更有效,因为它避免了在设置
cachedValue
后不必要地锁定mutex
可以保证昂贵的计算不会执行多次
另外,cacheValid
标志必须是原子的
,因为它在写入和读取cachedValue
之间创建了“发生在之前”的关系。
换句话说,它将cachedValue
(在mutex
之外访问)与调用magicValue()
的其他线程同步。
如果cacheValid
是一个常规的“bool”,那么在cacheValid
和cachedValue
上都会有数据竞争(根据C++11标准会导致未定义的行为)
在cacheValid
内存操作上使用默认的顺序一致内存顺序是可以的,因为这意味着获取/释放语义。
理论上,您可以通过在原子加载和存储上使用较弱的内存顺序进行优化:
int Widget::magicValue() const
{
if (cacheValid.load(std::memory_order_acquire)) return cachedValue;
else {
std::lock_guard<std::mutex> guard(m); // lock m
if (cacheValid.load(std::memory_order_relaxed)) return cachedValue;
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid.store(true, std::memory_order_release);
return cachedValue;
}
}
int小部件::magicValue()常量
{
if(cacheValid.load)(标准::内存\u顺序\u ac