乐趣区

关于c++:C单例模式双重锁漏洞内存读写的乱序执行编译器问题

阐明

首先解释为什么会呈现这个问题:在懒汉模式下存在多线程不平安的问题(饿汉模式是线程平安的),为了解决这个问题,首先采纳的是利用 C ++ 中 lock_guard 类,这个类实现原理采纳 RAII,不必手动治理 unlock。

class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {lock_guard<mutex> guard(lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}

解决了懒汉模式的这个问题,又呈现了性能问题:每次执行都会有加锁开释锁,而这个步骤只有在第一次 new Singleton()才是有必要的.

因而引出 DCL

双重查看锁模式

class singleton {
private:
    singleton() {}

    static singleton *p;
    static mutex lock_;
public:
    singleton *instance();

    // 实现一个内嵌垃圾回收类
    class CGarbo
    {
    public:
        ~CGarbo()
        {if(singleton::p)
                delete singleton::p;
        }
    };
    // 2022-4-21
    // 所谓的垃圾回收,就是定义一个动态的类,类外面只有一个办法,这个办法查看 singleton::p 是否是空,不是空,执行 delete
    // 因为是动态的办法,程序会主动的开释该对象,从而达到主动回收的机制。static CGarbo Garbo; // 定义一个动态成员变量,程序完结时,零碎会主动调用它的析构函数从而开释单例对象
};

singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;

singleton* singleton::instance() {if (p == nullptr) {lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();}
    return p;
}

留神咱们的题目是 单例模式双重锁破绽

DCLP 的关键在于,大多数对 instance 的调用会看到 p 是非空的,因而甚至不必尝试去初始化它。因而,DCLP 在尝试获取锁之前查看 p 是否为空。只有当查看胜利(也就是 p 还没有被初始化)时才会去取得锁,而后再次查看 p 是否依然为空(因而命名为双重查看锁)。第二次查看是必要,因为就像咱们刚刚看到的,很有可能另一个线程偶尔在第一次查看之后,取得锁胜利之前初始化 p。

看起来上述代码十分美妙,可是过了相当一段时间后,才发现这个破绽,起因是:内存读写的乱序执行(编译器问题)。

再次思考初始化 p 的那一行:

p = new singleton;
这条语句会导致三个事件的产生:

调配可能存储 singleton 对象的内存;
在被调配的内存中结构一个 singleton 对象;
让 p 指向这块被调配的内存。
可能会认为这三个步骤是按程序执行的,但实际上只能确定步骤 1 是最先执行的,步骤 2,3 却不肯定。问题就呈现在这。

线程 A 调用 instance,执行第一次 p 的测试,取得锁,依照 1,3, 执行,而后被挂起。此时 p 是非空的,然而 p 指向的内存中还没有 Singleton 对象被结构。
线程 B 调用 instance,断定 p 非空,将其返回给 instance 的调用者。调用者对指针解援用以取得 singleton,噢,一个还没有被结构出的对象。bug 就呈现了。
DCLP 可能良好的工作仅当步骤一和二在步骤三之前被执行,然而并没有办法在 C 或 C ++ 中表白这种限度。这就像是插在 DCLP 心脏上的一把匕首:咱们须要在绝对指令程序上定义限度,然而咱们的语言没有给出表白这种限度的办法。

第一种解决办法:memory barrier 指令

第一种实现:

基于 operator new+placement new,遵循 1,2,3 执行程序顺次编写代码。

// method 1 operator new + placement new
singleton *instance() {if (p == nullptr) {lock_guard<mutex> guard(lock_);
        if (p == nullptr) {singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
            new(tmp)singleton();
            p = tmp;
        }
    }
    return p;
}

第二种实现:

基于间接嵌入 ASM 汇编指令 mfence,uninx 的 barrier 宏也是通过该指令实现的。

#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {if (p == nullptr) {lock_guard<mutex> guard(lock_);
        barrier();
        if (p == nullptr) {p = new singleton();
        }
    }
    return p;
}
退出移动版