【问题标题】:Effective placement of lock_guard - from item 16 from Effective Modern C++lock_guard 的有效放置 - 来自 Effective Modern C++ 的第 16 项
【发布时间】:2017-01-25 00:44:49
【问题描述】:

在第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 总是比互斥锁快。

[编辑]

为了完整起见,我测量了两个方法的效率,第二个看起来只快 6%。:http://coliru.stacked-crooked.com/a/e8ce9c3cfd3a4019

【问题讨论】:

  • 因为读取cacheValid可能不是原子的?
  • @NeilButterworth:欢迎回来。
  • 您明白,您的计算可以使用第二个版本多次运行
  • @steve 其实我没有,你能写出来回答为什么吗?我认为将 cacheValid 设置为 std::atomic 就足够了,还有一个 lock_guard 我认为应该防止双重计算。
  • 你检查cacheValid是否为假,代码将切换到else情况,此时线程被中断。下一个线程检查 cacheValid 仍然为假,转到 else 情况.......

标签: c++ multithreading atomic


【解决方案1】:

您的第二个代码 sn-p 显示了 双重检查锁定模式 (DCLP) 的完全有效实现,并且(可能)比 Meyers 的解决方案更有效,因为它避免了不必要地锁定 mutex在设置cachedValue 之后。

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

此外,cacheValid 标志为 atomic 也很重要,因为它在写入和读取 cachedValue 之间创建了 发生前 关系。 换句话说,它将cachedValue(在mutex 之外访问)与其他调用magicValue() 的线程同步。 如果 cacheValid 是一个常规的“布尔”,您将在 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 无效并重新开始计算。但这不是迈耶斯示例的一部分。

【讨论】:

    【解决方案2】:

    我实际上认为你的 sn-p 是正确的,但它依赖于一个在现实世界的例子中通常不正确的假设:它假设 cacheValid 从假到真,但永远不能逆向前进,即失效。

    在旧代码中,mutex 保护 所有 读取 写入 cachedValue。在您的新代码中,实际上在互斥锁之外有 cachedValue 的读取访问权限。这意味着一个线程可以读取这个值,而另一个线程正在写入它。问题是只有在cacheValid 为真时才会在互斥锁之外进行读取。但是如果cacheValid 为真,则不会发生写入; cacheValid 只能在所有写入完成后变为真(注意这是强制的,因为cacheValid 上的赋值运算符将使用最严格的内存排序保证,因此不能与前面的重新排序块中的指令)。

    但是假设编写了一些其他代码,可以使缓存无效:Widget::invalidateCache()。这段代码除了将cacheValid 再次设置为false 之外什么也不做。在旧代码中,如果您从不同的线程重复调用invalidateCachemagicValue,则后一个函数可能会在任何给定点重新计算值或不重新计算。但是,即使您的复杂计算每次调用时都返回不同的值(例如,因为它们使用全局状态),您总是会得到旧值或新值,而没有其他值。但现在考虑一下代码中的以下执行顺序:

    1. 线程 1 调用magicValue,并检查cacheValid 的值。这是真的。它在继续之前被中断。
    2. 线程 2 调用invalidateCache,然后立即调用magicValuemagicValue 看到缓存无效,获取互斥体,开始计算,开始写入cacheValid
    3. 线程 1 中断,读取部分写入的 cacheValid

    我实际上不认为这个示例适用于大多数现代计算机,因为int 通常是 32 位,并且通常 32 位写入和读取将是原子的。因此,实际上不可能散布或“撕裂”cachedValue 的值。但是在不同的架构上,或者如果您使用整数以外的类型(例如,任何超过 64 位的类型),则不能保证写入或读取是原子的。因此,作为magicValue 的返回,您可以获得既不是旧值也不是新值,而是一些奇怪的按位混合,甚至不是有效对象。

    所以,很高兴你能找到这个。我想,为了简单起见,作者在尝试简化示例时,忘记了不再需要严格将互斥锁放在外面。

    【讨论】:

    • 你是对的,在大多数平台上,整数上不可能有撕裂的读取或写入。但是将标志设置为无效的另一个问题是 cachedValue (非原子)现在可以同时读取和写入。这是一场行为未定义的数据竞赛
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-02
    • 1970-01-01
    • 1970-01-01
    • 2021-12-31
    相关资源
    最近更新 更多