在第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
};
我想知道为什么应该在每次magicValue()调用时始终执行std :: lock_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
};
这样就需要更少的互斥锁,从而提高代码的效率。我在这里假设atomica总是比互斥量更快。
[编辑]
为了完整性,我测量了两个apraches的效率,第二个看起来只有6%更快:http://coliru.stacked-crooked.com/a/e8ce9c3cfd3a4019
答案 0 :(得分:3)
您的第二个代码段显示了 Double Checked Locking Pattern (DCLP)的完全有效的实现,并且(可能)Meyers的解决方案(可能)更有效,因为它避免了不必要地锁定mutex
设置cachedValue
后。
保证昂贵的计算不会多次执行。
此外,cacheValid
标志为atomic
非常重要,因为它会在写入和阅读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
无效并重新开始计算。但这不是迈耶斯的例子。
答案 1 :(得分:2)
我实际上认为你的代码片段是孤立的,但它依赖于一个在现实世界的例子中通常不正确的假设:它假设cacheValid
从false变为true,但永远不会反向进展,即失效。
在旧代码中,mutex
保护所有在cachedValue
上读取和写入。在您的新代码中,实际上对互斥锁之外的cachedValue
具有读访问权限。这意味着一个线程可以读取该值,而另一个线程正在编写它。问题在于,只有在cacheValid
为真时才会在互斥锁之外进行读取。但如果cacheValid
为真,则不会发生任何写作; cacheValid
只有在所有写入完成后才能成为(请注意,这是强制执行的,因为cacheValid
上的赋值运算符将使用最严格的内存排序保证,因此它不能按照块中早先的说明重新排序。
但是假设编写了一些其他代码,可能会使缓存失效:Widget::invalidateCache()
。这段代码除了将cacheValid
再次设置为false之外什么都不做。在旧代码中,如果您从不同的线程重复调用invalidateCache
和magicValue
,后一个函数可能会在任何给定点重新计算值。但是,即使你的复杂计算每次被调用时返回不同的值(因为它们使用全局状态,比如说),你将始终获得旧值或新值,而不是其他值。但现在考虑代码中的以下执行顺序:
magicValue
,并检查cacheValid
的值。这是真的。它会在继续之前被中断。invalidateCache
,然后立即调用magicValue
。 magicValue
发现缓存无效,获取互斥锁并开始计算,开始写入cacheValid
。cacheValid
。我实际上并不认为此示例适用于大多数现代计算机,因为int
通常为32位,通常32位写入和读取将是原子的。所以它不可能散布或者撕裂&#34; cachedValue
的值。但是在不同的体系结构上,或者如果你使用的不是整数类型(例如64位以上的类型),写入或读取不能保证是原子的。因此,作为magicValue
的返回,你可以获得既不是旧值也不是新值的东西,而是一些甚至不是有效对象的奇怪的按位混合。
所以,擅长找到这个。我想,为了简单起见这个例子,作者忘了不再需要严格把互斥体放在外面。