IE盒子

搜索
查看: 134|回复: 0

Modern C++ | 军训第十六条: 使 const 成员函数线程安全

[复制链接]

3

主题

8

帖子

15

积分

新手上路

Rank: 1

积分
15
发表于 2023-1-15 13:24:19 | 显示全部楼层 |阅读模式
重点

  • 使 const 成员函数线程安全, 除非确定它们永远不会在并发的情况中使用.
  • 使用 std::atomic 变量可能会提供比互斥锁(mutex)更好的性能, 但它们仅适用于操作单个变量或内存位置.
假设我们有一个可以计算多项式根的 Polynomial 类. 并且为了避免冗余计算, 我们将计算后的结果缓存起来. 这个计算显然是一个 const 函数. 我们想出了第一个版本的代码:
class Polynomial {
public:
  using RootsType = std::vector<double>;

  RootsType roots() const {
    if (!rootsAreValid) { // if cache not valid
      ... // computation
      rootsAreValid = true;
    }
    return rootVals;
  }

private:
  mutable bool rootsAreValid{ false };
  mutable RootsType rootVals{};
};
但是, 当两个线程同时调用 roots() 函数时, 它们可能最终都在进行计算, 并且 rootVals 可能包含重复的根.
Polynomial p;
...
/* Thread 1 */                         /* Thread 2 */
auto rootsOfP1 = p.roots();            auto rootsOfP2 = p.roots();
此处的问题是, roots() 被声明为 const 但不是线程安全的. 解决这个问题的一种最简单的方法是使用互斥锁(mutex):
class Polynomial {
public:
  using RootsType = std::vector<double>;

  RootsType roots() const {
    std::lock_guard<std::mutex> g(m);        // lock mutex
    if (!rootsAreValid) {                 // if cache not valid
      ... // computation
      rootsAreValid = true;
    }
    return rootVals;
  }

private:
  mutable std::mutex m;
  mutable bool rootsAreValid{ false };
  mutable RootsType rootVals{};
};
但是, 有时互斥量可能有点矫枉过正. 例如, 对于一个简单的计数器, 一个 std::atomic 计数器将是完成这项工作的一种更轻便高效的方式:
class Point {
public:
  ...
  double distanceFromOrigin() const noexcept {
    ++callCount;
    ...
    return std::hypot(x, y);
  }
private:
  mutable std::atmoic<unsigned> callCount{ 0 };
  double x, y;
}
请记住, std::mutex std::atomic 都是不可复制和不可移动的, 因此包含它们的类也是不可复制和不可移动的.
由于对 std::atmoic 的操作比 std::mutex 的获取和释放更高效轻便, 我们可能会想大量使用 std::atmoic. 然而, 它并不是那么普遍适用. 来看一个反例:
class Widget {
public:
  ...
  int magicValue() const {
    if (cacheValid) return cachedValue;
    else {
      auto val1 = expensiveComputation1();
      auto val2 = expensiveComputation2();
      cachedValue = val1 + val2;        // first compute and assign
      cacheValid = true;                // then validate cache
      return cachedValue;
    }
  }
private:
  mutable std::atmoic<bool> cacheValid{ false };
  mutable std::atmoic<int> cachedValue;
};
上面代码的问题是两个线程可能同时进入else分支, 重复计算. 你可能会想:"好吧,那我就先把bool设为true再进行计算". 但这仍然不对.
class Widget {
public:
  ...
  int magicValue() const {
    if (cacheValid) return cachedValue;
    else {
      auto val1 = expensiveComputation1();
      auto val2 = expensiveComputation2();
      cacheValid = true;                // first validate cache
      cachedValue = val1 + val2;        // then compute and assign
      return cachedValue;
    }
  }
private:
  mutable std::atmoic<bool> cacheValid{ false };
  mutable std::atmoic<int> cachedValue;
};
一个线程可能首先把缓存设成true并开始计算过程, 而另一个线程看到缓存有效并从 cachedValue 中获取未定义的值, 这肯定会导致未定义的程序运行行为.
在这种情况下, 正确的解决方案是 std::mutex 来保护整个关键阶段(critical session).
此处的重点是, 对于需要同步的单个变量或内存位置, 使用 std::atomic 就足够了. 但是一旦遇到两个或更多需要作为一个单元进行操作的变量或内存位置, 应该使用 std ::mutex 互斥锁.
<hr/>欢迎大家指出不足或错误, 进行提问和讨论.
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表