C++ 锁护板的有效位置-从有效现代C和x2B的第16项开始+;

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();

在第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
};
类小部件{
公众:
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
,则后一个函数可能会在任何给定点重新计算值,也可能不会重新计算值。但即使您的复杂计算在每次调用时都返回不同的值(因为它们使用全局状态),您也总是会得到旧值或新值,而不会得到其他值。但是现在考虑以下代码中的执行顺序:

  • 线程1调用
    magicValue
    ,并检查
    cacheValid
    的值。这是真的。它在继续之前就被打断了
  • 线程2调用
    invalidateCache
    ,然后立即调用
    magicValue
    magicValue
    发现缓存无效,获取互斥锁,开始计算,并开始写入
    cacheValid
  • 线程1中断,读取部分写入的
    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