玩命加载中 . . .

单例模式


单例分为两种实现方法:

  • 饿汉:单例类定义的时候就进行了实例化,是线程安全的
// 饿汉
class Singleton {
private:
    Singleton() {}
    static Singleton *p;
public:
    static Singleton* instance();
};

Singleton* Singleton::p = new Singleton();  // 定义的时候就实例化

Singleton* Singleton::instance() {
    return p;
}
  • 懒汉:第一次用到类实例的时候才会去实例化
// 懒汉
class Singleton {
private:
    Singleton() {}
    static Singleton *p;    // 静态指针指向唯一的实例
public:
    static Singleton* instance();
};

Singleton* Singleton::p = nullptr;

Singleton* Singleton::instance() {
    if (p == nullptr) {
        p = new Singleton();    // 调用函数才实例化
    }
    return p;
}

将构造函数声明为privateprotected防止被外部函数实例化,内部有一个『静态的类指针保存唯一的实例』,实例的实现由一个public方法来实现,该方法返回该类的唯一实例。

当然这个代码只适合在单线程下,当多线程时,是不安全的。考虑两个线程同时首次调用instance方法且同时检测到p==nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。

多线程加锁

在C++中加锁有个类实现原理采用RAII,不用手动管理unlock,那就是lock_guard,这里采用其进行加锁

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;
}

这种写法不会出现上面两个线程都执行到p==nullptr里面的情况,当线程A在执行p = new Singleton()的时候,线程B如果调用了instance(),一定会被阻塞在加锁处,等待线程A执行结束后释放这个锁。从而是线程安全的

但是这种写法性能非常低下,因为每次调用instance()都会加锁释放锁,而这个步骤只有在第一次new Singleton()才是有必要的,只要p被创建出来了,不管多少线程同时访问,使用if (p == nullptr)进行判断都是足够的(只是读操作,不需要加锁),没有线程安全问题,加了锁之后反而存在性能问题

双重检查锁模式

对于读操作是不存在线程安全的,故只需要在第一次实例创建的时候加锁,以后不需要

双重检查锁

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

Singleton *Singleton::p = nullptr;

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是否仍然为空(因此命名为双重检查锁

第二次检查是必要,因为可能线程A判断p为空,还没拿到锁,线程B快一步判断p为空,拿到锁实例化并释放锁,此时如果线程A没有判断,就会直接实例化,从而违反单例准则,所以第二次的检查是必要的

看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞,原因是:内存读写的乱序执行(编译器问题)

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

p = new Singleton();

这条语句会导致三个事情的发生:

  1. 分配能够存储Singleton对象的内存
  2. 在被分配的内存中构造一个Singleton对象
  3. p指向这块被分配的内存

可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤2,3却不一定。问题就出现在这

线程A调用instance,执行第一次p的检查,获得锁,按照1,3执行,然后被挂起。此时p是非空的,但是p指向的内存中还没有Singleton对象被构造
线程B调用instance,判定p非空, 将其返回给instance的调用者。调用者对指针解引用以获得Singleton,但指针所指的是一个还没有被构造出的对象。bug就出现了
DCLP能够良好的工作仅当步骤一和二在步骤三之前被执行,但是并没有方法在C或C++中表达这种限制。我们需要在相对指令顺序上定义限制,但是我们的语言没有给出表达这种限制的方法

memory barrier指令

DCLP问题在C++11中,这个问题得到了解决。

在C++11之前,解决方法是barrier指令。要使其正确执行的话,就得在步骤2、3直接加上一道memory barrier。强迫CPU执行的时候按照1、2、3的步骤来运行。

第一种实现:基于operator new+placement new,遵循1、2、3执行顺序依次编写代码

Singleton *instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr) {
            Singleton *tmp = static_cast<Singleton*>(operator new(sizeof(Singleton)));  // 1. 分配内存
            new(tmp)Singleton();    // 2. 构建对象实例
            p = tmp;    // 3. 指针指向实例
        }
    }
    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;
}

通常情况下是调用cpu提供的一条指令,这条指令的作用是会阻止cpu将该指令之前的指令交换到该指令之后,这条指令也通常被叫做barrier。 上面代码中的asm表示这个是一条汇编指令,volatile是可选的,如果用了它,则表示向编译器声明不允许对该汇编指令进行优化。lwsyncPOWERPC提供的barrier指令。

静态局部变量

新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,不过在C++11下有更简洁的多线程Singleton写法

class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&);
    const Singleton& operator=(const Singleton&);
    string name;
public:
    static Singleton* instance();
    string getName() { return name; }
    void setName(string inputName) { name = inputName; }
};

Singleton* Singleton::instance() {
    static Singleton p;
    return &p;
}

int main() {
    Singleton* ptr = Singleton::instance();
    ptr->setName("kavin");
    cout << ptr->getName() << endl;
    cout << Singleton::instance()->getName() << endl;
    return 0;
}
  • 单线程下,正确
  • C++11及以后的版本(如C++14)的多线程下,正确
  • C++11之前的多线程下,不一定正确

原因在于在C++11之前的标准中并没有规定local static变量的内存模型。所以就不是线程安全的了。但是在C++11却是线程安全的,这是因为新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它


文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录