QuantLib 金融计算——一个线程安全隐患
概述
C++ 11 后标准库引入了 thread
以实现并行计算。与 OpenMP 不同,thread
对线程的操作属于粗粒度的,适合将若干耗时的计算任务先做独立封装,再并行运行。以蒙特卡洛方法为例,敏感性的计算需要反复进行模拟定价,而这些定价计算则可以借助 thread
并行化。
一个线程安全隐患
QuantLib 当前的设计存在着一个不易发觉的线程安全隐患,即 LazyObject
中避免重复计算的机制。LazyObject
用模板方法模式实现了 calculate
,而 calculate
会修改 calculate_
的值(一个 mutable
变量)。并行计算时可能在此处触发数据竞争,这导致 LazyObject
的派生类都是线程不安全的。
例如,最常用的期限结构类 FlatForward
就是 LazyObject
的派生类,若并行计算任务直接或间接共用了同一个 FlatForward
对象,则可能引发程序崩溃。
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_
的值为 false
。
还是以 FlatForward
为例,calculate
在 discountImpl
中被调用,而计算零息利率、远期利率和贴现因子的方法都会调用 discountImpl
。随便算一个贴现因子就可以使 calculated_
的值为 false
。