在第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();
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
};
我想知道为什么 std::lock_guard 应该总是在每次 magicValue() 调用时执行,不会像预期的那样工作吗?:
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
};
这样就需要更少的互斥锁,从而提高代码效率。我在这里假设 atomica 总是比互斥锁快。
[编辑]
为了完整性,我测量了两个 aprache 的效率,第二个看起来只快了 6%。:http://coliru.stacked-crooked.com/a/e8ce9c3cfd3a4019
最佳答案
您的第二个代码片段显示了双重检查锁定模式 (DCLP) 的完全有效实现并且(可能)比 Meyers 的解决方案更有效,因为它避免了锁定 mutex
在 cachedValue
设置后不必要的。
保证不会多次执行昂贵的计算。
此外,cacheValid
标志是atomic
也很重要,因为它在写入和读取之间创建了一个happens-before 关系来自 cachedValue
。
换句话说,它将 cachedValue
(在 mutex
之外访问)与调用 magicValue()
的其他线程同步。
如果 cacheValid
是一个常规的“bool”,您将在 cacheValid
和 cachedValue
上发生数据竞争(根据 C+ 导致未定义的行为+11 标准)。
在 cacheValid
内存操作上使用默认顺序一致的内存排序很好,因为它暗示了获取/释放语义。
理论上,您可以通过在 atomic
加载和存储上使用较弱的内存顺序来优化:
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;
}
}
请注意,这只是一个较小的优化,因为读取 atomic
是许多平台上的常规负载(使其与从非原子读取一样高效)。
正如 Nir Friedman 所指出的,这只能以一种方式起作用;您不能使 cacheValid
无效并重新开始计算。但这不是迈耶斯示例的一部分。
关于c++ - Effective placement of lock_guard - 来自 Effective Modern C++ 的第 16 条,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41841572/