c++ - Effective placement of lock_guard - 来自 Effective Modern C++ 的第 16 条

标签 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();
      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 的解决方案更有效,因为它避免了锁定 mutexcachedValue 设置后不必要的。

保证不会多次执行昂贵的计算。

此外,cacheValid 标志是atomic 也很重要,因为它在写入和读取之间创建了一个happens-before 关系来自 cachedValue。 换句话说,它将 cachedValue(在 mutex 之外访问)与调用 magicValue() 的其他线程同步。 如果 cacheValid 是一个常规的“bool”,您将在 cacheValidcachedValue 上发生数据竞争(根据 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/

相关文章:

使用 std::vector 从 dll 调用其他 dll 的 C++

C# - 在不同的线程中添加 UserControl 即使在调用之后也会导致异常

c - 关于 c struct (gcc) 问题的原子加载/保存

c++ - 如图所示,使用这些具有松弛的内存顺序和 rel/acq 的原子操作,此 C++ 代码段是否有效?

c++ - 删除另一个类的方法中的多个指针

C++ 随机快速排序段错误

c# - 确定 NamedDataSlot 是否存在的最佳方法是什么

c++ - 调用 sf::Window::close 后 SFML 中的段错误

C++:内存映射文件上的 Fetch_add

c++ - 使用动态规划从背包中检索项目