文章

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 为例,calculatediscountImpl 中被调用,而计算零息利率、远期利率和贴现因子的方法都会调用 discountImpl。随便算一次贴现因子就可以使 calculated_ 的值为 true

本文由作者按照 CC BY 4.0 进行授权