EMCPP条款16:保证const成员函数的线程安全性
在数学领域中,使用一个类来表示多项式会非常方便。而在这个类中,若有一个函数能够计算多项式的根,即那些使得多项式求值结果为零的值,将会很有用。这样一个函数并不会造成多项式的值的改动,因此将它声明为 const
成员函数也很自然。
计算多项式的根也许代价高昂,在不得不计算多项式的根时,我们就把这些根缓存起来,并以返回缓存值的手法来实现 roots
。以下是一种基本的做法:
1 | class Polynomial { |
从概念上说,roots
不会改变它操作的 Polynomial
对象,然而作为缓存活动的组成部分,它可能需要修改 rootVals
和 rootsAreValid
的值。这是 mutable
的经典用例, 也是它为何被加到数据成员声明中。
设想现在有两个线程同时在同 一个 Polynomial
对象上调用 roots
:
1 | /*----- Thread 1 ----- */ /*------- Thread 2 ------- */ |
问题就在于,roots
被声明成了 const
函数,但却并非线程安全的。要解决这个问题,最简单的办法也是最常见的:引入一个 mutex
:
1 | class Polynomial { |
值得关注的是,由于 std::mutex
是个只移型别 (move-only type) (即只能移动但不能复制的型别),将 m
加入 Polynomial
的副作用就是 Polynomial
失去了可复制性。不过,它仍然可移动。
就一些特定情况而言,引入互斥址是杀鸡用牛刀之举。以下代码演示了如何使用 std::atomic
型别的对象来计算调用次数:
1 | class Point { // 2D point |
由于对 std::atomic
型别的变量的操作与加上与解除互斥量相比,开销往往比较小,你也许应该尝试比惯常程度更重度地依靠 std::atomic
型别的对象,例如,如果某类需要缓存计算开销较大的 int
型别的变最,则应该尝试使用一对 std::atomic
型别的变量来取代互斥量。
1 | class Widget { |
但会存在两个大开销计算在不同的线程被同时执行的问题,因为 cacheValid
可能会被同时观察为 false
。这里我们学到的教益是:对于单个要求同步的变量或内存区域,使用 std::atomic
就足够了。但是如果有两个或更多个变量或内存区域需要作为一整个单位进行操作时,就要动用互斥量了
要点速记
- 保证
const
成员函数的线程安全性,除非可以确信它们不会用在并发语境中。 - 运用
std::atomic
型别的变量会比运用互斥量提供更好的性能,但前者仅适用对单个变量或内存区域的操作。