有一个类来表示多项式会非常方便,而在这个类中,其中有一个函数能够计算多项式的根,即那些使得多项式求值结果为零的数值;在计算多项式的根时,我们就把这些根缓存起来,并以返回缓存数值的手法实现roots
class Polynomial {
public:
using RootsType = std::vector<double>; // 持有值的数据结构,这些数值使得多项式求值结果为零
RootsType roots() const {
if (!rootsAreValid) { // 如果缓存无效
//...
rootsAreVali d = true; // 则计算根,将其存入rootVals
}
}
private:
mutable bool rootsAreValid {false}; // 关于初始化信息
mutable RootsType rootVals{}; // 条款7
};
从理论上说,roots
不会改变它操作的Polynomial
对象,然而作为缓存活动组成部分,它可能需要修改rootVals
和rootsAreValid
的数值,这是mutable
的经典用例;
设想现在有两个线程同时在同一个Polynomial
对象上调用roots
:
Ploynomial p;
auto rootsOfP = p.roots(); // 线程1
auto valsGivingZero = p.roots();// 线程2
上述代码在本例中不安全,因为在roots
内部,这些线程中一个或者两个可能企图更改数据成员rootsAreaValid
和rootVals
,这就意味着这段代码可能有不同的多个线程在没有同步的情况下读写同一块内存,而这就是数据竞险。解决办法是引入一个mutex
class Polynomial {
public:
using RootsType = std::vector<double>; // 持有值的数据结构,这些数值使得多项式求值结果为零
RootsType roots() const {
std::lock_guard<std::mutex> g(m); // 加上互斥量
if (!rootsAreValid) { // 如果缓存无效
//...
rootsAreValid = true; // 则计算根,将其存入rootVals
}
} // 解决互斥量
private:
mutable std::mutex m;
mutable bool rootsAreValid {false}; // 关于初始化信息
mutable RootsType rootVals{}; // 条款7
};
之所以要把std::mutex m
声明为mutable
,是因为加锁与解锁都不是const
成员函数所为;
如果要计算一个成员函数被调用的次数,使用std::atomic
类型的计数器将会是一种成本较低的途径
class Point { // 表示2D点
public:
double distanceFromOrigin() const noexcept {
++callCount; // 带有原子性的自增操作
return std::sqrt((x * x) + (y * y));
}
private:
mutable std::atomic<unsigned> callCount{0};
double x, y;
};
如果某类需要缓存计算开销较大的int
类型变量,则应该尝试使用一对std::atomic
类型的变量来取代互斥量。
class Widget {
public:
int magicValue() const {
if (cacheValid) {
return cacheValue;
} else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValue = val1 + val2; // 第一部分
cacheValid = true; // 第二部分
return cacheValue;
}
}
private:
mutable std::atomic<bool> cacheValid{false};
mutable std::atomic<int> cacheValue;
};
然后考虑如下调用:
- 一个线程调用
Widget::magicValue
时,观察到cacheValid
数值为false
,于是执行了两个大开销的计算,并将其和赋值给了cacheValue
- 与此同时,另一个线程也在调用
Widget::magicValue
,也观察到cacheValid
值为false
,于是也执行了第一个线程刚刚完成的两次同样的大开销运算
这种行为与缓存的目标南辕北辙,颠倒对cacheValid
和cacheValue
的赋值顺序可以消除该问题,但是结果却更坏了
class Widget {
public:
int magicValue() const {
if (cacheValid) {
return cacheValue;
} else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValid = true; // 第一部分
return cacheValue = val1 + val2; // 第二部分
}
}
private:
mutable std::atomic<bool> cacheValid{false};
mutable std::atomic<int> cacheValue;
};
假设cacheValid
的数值为false
,接着:
- 一个线程调用
Widget::magicValue
并执行到了cacheValid
值被置为true
的时刻 - 在这一时刻,另一个线程也在调用
Widget::magicValue
,并检视cacheValid
的值。观察到其数值为true
后,该线程就把cacheValid
的数值返回了,即使此时第一个线程还没有执行对cacheValid
的赋值。因此返回值是不正确的。
这里学到的教益是:对于单个要求同步的变量或内存区域,使用std::atomic
就够了。但是如果有两个或更多变量或内存区域需要作为一整个单位进行操作,就要使用互斥量了。
class Widget {
public:
int magicValue() const {
std::lock_guard<std::mutex> guard(m); // 为m加上互斥量
if (cacheValid) {
return cacheValue;
} else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cacheValid = true; // 第一部分
return cacheValue = val1 + val2; // 第二部分
}
} // 为m解除互斥量
private:
mutable std::mutex m;
mutable std::atomic<bool> cacheValid{false}; // 不再具备原子性
mutable std::atomic<int> cacheValue; // 不再具备原子性
};