首页 > Note > 从单例模式谈起(一)

从单例模式谈起(一)

2019年4月8日 发表评论 阅读评论

单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 C++ 中,由单例模式可以引出一系列的问题,可能会比较有意思,这里探讨一下。

常见的简单实现

1.使用 static 实现

其中 S2 是要避免的。因为 1. 类的静态成员变量的初始化时间一般早于 main 函数 2. 如果静态成员的初始化里调用了其它类,可能出现未定义的错误。

2.使用指针判断是否初始化

这两种方式在单线程程序中使用都是可以的,但如果在多线程中,就会出现问题。

Magic Static

对于 S1 要注意, C++ 局部静态变量的初始化可能不是线程安全的 , 这就是 Magic Static ,是指 返回一个静态局部变量的引用 的用法,在某些情况下,如S1 可能会被编译器这样解析:

这不是线程安全的。这里的 "某些情况" 是指 C++11之前的编译器。尽管C++11 标准规定这个过程应该是线程安全的,但是在 VS2015 之前,微软还是未实现这个标准。参见这里
Thread-Safe "Magic" Statics Static local variables are now initialized in a thread-safe way, eliminating the need for manual synchronization. Only initialization is thread-safe, use of static local variables by multiple threads must still be manually synchronized. The thread-safe statics feature can be disabled by using the /Zc:threadSafeInit- flag to avoid taking a dependency on the CRT. (C++11)
以及这里这里

Double-Checked Locking Problem

对于 S3, 在多线程中, "s==0" 这个条件可能会被多个线程判断为 true, 这会导致 s 被多次初始化, 而只有最后一次初始化有效, 那么对其改进如下:

这是线程安全的,但引出的问题是,每次都会调用该方法都要加解一次锁,加解锁的过程是很耗资源的。那么对于上面的代码可以有如下改进:

这段代码解决了每次调用都要加解锁问题。但它仍然不是线程安全的。编译器可能会将代码解析成这样:

但也可能解析成这样:

对于后一种情况,它将先为 s 分配一段内存,再将为它构建对象: 它不是线程安全的。

如图,当线程1为 s分配内存后,线程2 判断 s==0 为flase, 此时线程2 将返回一个已分配内存但未初始化的指针, 可能会引起程序崩溃。

volatile

在网上有很多帖子认为使用 volatile 可以解决 DCLP , 实际情况是, 使用 volatile 无法完美解决这个问题。这个我们下一节再作讨论。

基于 C++11 的解决方案

C++11 为我们提供了很多方便好用的同步原语,可以帮助我们解决前面提到的问题。

atomic

将变量声明为原子的,可以确保双检锁正确执行:

callonce

使用 callonce /onceflag 则是一种更优雅的方式:

使用模板的单例模式

更多的时候, 我们是仅定义一个单例模式的基类, 使需要使用单例的类来继承这个基类。定义如下:

多参数构造函数的单例模式

此时必须注意,需要和无参数的单例模式分开来。