const 成员函数中的互斥锁或原子

Posted

技术标签:

【中文标题】const 成员函数中的互斥锁或原子【英文标题】:mutex or atomic in const member function 【发布时间】:2017-01-11 22:33:12 【问题描述】:

我正在阅读 Scott Meyers 的 Effective Modern C++ 中的第 16 条。 在项目的后半部分,他说

对于需要同步的单个变量或内存位置,使用std::atomic 就足够了,但是一旦您到达需要作为一个单元进行操作的两个或更多变量或内存位置,您应该使用 互斥体。

但我还是不明白为什么在单个变量或内存位置的情况下就足够了,以本项中的多项式为例

class Polynomial 
 public:
 using RootsType = std::vector<double>;
 RootsType roots() const
 
   if (!rootsAreValid)  // if cache not valid
    .... // **very expensive compuation**, computing roots,
         // store them in rootVals
   rootsAreValid = true;
  
 return rootVals;

private:
mutable  std::atomic<bool> rootsAreValid false ;
mutable RootsType rootVals;
;

我的问题是:

如果线程 1 在将rootAreValid 分配给true 之前正在大量计算rootVals,并且线程2 也调用函数roots(),并将rootAreValid 计算为false,则线程 2 也将进入 rootVals 的繁重计算,所以对于这种情况,原子 bool 是如何足够的?我仍然认为需要std::lock_guard&lt;mutex&gt; 来保护rootVals 计算的入口。

【问题讨论】:

显然是不够的。引用说“对于需要同步的单个变量或内存位置......” @juanchopanza,嗯,这不是“对于需要同步的单个变量或内存位置”的例子吗? 不行,你也需要同步向量。 真的在这种情况下,您需要另一个标志来检查是否有线程正在运行昂贵的计算。这是书中的例子还是你自己的场景? @Allanqunzi 这是有道理的。但是,您将代码表示为直接来自书籍而不是修改。 【参考方案1】:

在您的示例中,有两个变量正在同步:rootValsrootsAreValid。该特定项目指的是 only 原子值需要同步的情况。例如:

#include <atomic>

class foo

public:
    void work()
    
        ++times_called;
        /* multiple threads call this to do work */
    
private:
    // Counts the number of times work() was called
    std::atomic<int> times_called0;
;

times_called 是本例中唯一的变量。

【讨论】:

我明白了,谢谢,我没有意识到有两个变量正在同步。【参考方案2】:

我建议您使用以下代码避免不必要的繁重计算:

class Polynomial 
 public:
 using RootsType = std::vector<double>;

 RootsType roots() const
 
   if (!rootsAreValid)  // Acquiring mutex usually is not cheap, so we check the state without locking
     std::lock_guard<std::mutex> lock_guard(sync);
     if (!rootsAreValid) // The state could changed because the mutex was not owned at the first check
     
       .... // **very expensive compuation**, computing roots,
           // store them in rootVals
     
     rootsAreValid = true;
  
  return rootVals;

private:
mutable std::mutex sync;
mutable std::atomic<bool> rootsAreValid false ;
mutable RootsType rootVals;
;

【讨论】:

检查rootsAreValid(一个普通的布尔值)是不受保护的(在互斥锁之外)这是一个典型的数据竞争的例子,因此是无效的。问题扩展到与写入它的线程不同步时返回的向量......这导致未定义的行为 @LWimsey,没有数据竞争。 rootsAreValid 的值不能从 true 更改为 false。当rootsAreValid 为假但这部分代码由互斥锁保护并进行额外比较时,当多个线程中的两个尝试计算向量时,可能会发生数据竞争。第一次检查没有保证的原因是性能,因为原子操作会消耗几十个滴答声。 您误解了什么是数据竞争:同时访问同一内存位置的多个线程(至少)其中一个是写入器.. 这适用于 @987654327 @ 并且它不能从 true 更改为 false 的事实是无关紧要的。是的,mutex 保证只有单个线程可以写入vector,但这并不能保证vector 正确同步到读取器线程,因为读取器访问在mutex 之外... .........(继续 (continued)............您的代码显示了 双重检查锁定模式的(无效)实现我>。如果您检查您的来源,您会发现rootsAreValid 不是bool,而是std::atomic&lt;bool&gt;(或等效项)。这将完全解决问题(std::atomic 是一种无数据竞争的类型),它将导致 vector 基于(默认)原子加载和存储的隐式获取/释放语义进行同步。跨度> @LWimsey, vector 不需要同步访问,因为它在初始化后没有更改。由于编译器优化,此模式的有效性是特定于编译器的。现代编译器应该没有问题。

以上是关于const 成员函数中的互斥锁或原子的主要内容,如果未能解决你的问题,请参考以下文章

对类中的成员函数的定义和声明最后添加一个const是什么意思?

哪个更有效,基本互斥锁或原子整数?

C++点滴----关于类常成员函数

函数后面有个 const

C++类中成员函数声明后面接 const

const成员函数总结