QuantLib 金融计算——一个线程安全隐患
这里记录 QuantLib 中观察者模式带来的一个线程安全隐患。
QuantLib 金融计算——一个线程安全隐患
由于版本问题,代码可能与最新版不兼容。
QuantLib 金融计算——一个线程安全隐患
概述
C++ 11 后标准库引入了 thread 以实现并行计算。与 OpenMP 不同,thread 对线程的操作属于粗粒度的,适合将若干耗时的计算任务先做独立封装,再并行运行。以蒙特卡洛方法为例,敏感性的计算需要反复进行模拟定价,而这些定价计算则可以借助 thread 并行化。
一个线程安全隐患
QuantLib 当前的设计存在着一个不易发觉的线程安全隐患,即 LazyObject 中避免重复计算的机制。LazyObject 用模板方法模式实现了 calculate,而 calculate 会修改 calculate_ 的值(一个 mutable 变量)。并行计算时可能在此处触发数据竞争,这导致 LazyObject 的派生类都是线程不安全的。
例如,最常用的期限结构类 FlatForward 就是 LazyObject 的派生类,若并行计算任务直接或间接共用了同一个 FlatForward 对象,则可能引发程序崩溃。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LazyObject : public virtual Observer,
public virtual Observable {
protected:
mutable bool calculated_;
virtual void performCalculations() const = 0;
public:
void update() { calculated_ = false; }
virtual void calculate() const {
if (!calculated_ && !frozen_) {
calculated_ = true; // prevent infinite recursion in
// case of bootstrapping
try {
performCalculations();
} catch (...) {
calculated_ = false;
throw;
}
}
}
};
解决办法
若要修改源代码,参考 Effective Modern C++ 第 16 条,可以为 calculate 方法加锁。
若不修改源代码,可以在并行计算开始前触发 LazyObject 的派生类对象的计算,使 calculated_ 的值为 true。
还是以 FlatForward 为例,calculate 在 discountImpl 中被调用,而计算零息利率、远期利率和贴现因子的方法都会调用 discountImpl。随便算一次贴现因子就可以使 calculated_ 的值为 true。
本文由作者按照 CC BY 4.0 进行授权