在 浅谈设计模式六: 单例模式(Singleton) 中提到 double-checked locking pattern(DCLP)来实现 Singleton 设计模式,但是在 C++11 之前,没有安全方法在可移植的 C++中去实现它.具体原因可见 单例模式(Singleton) 或 Scott Meyers 和 Andrei Alexandrescu 发布的原文 “C++ and the Perils of Double-Checked Locking” .
C++11 引入了新的内存模型和线程库,使得能在 C++中实现可移植的 DCLP.本文说明如何实现它.
什么是 Double-Checked Locking
在 单例模式(Singleton) 很好的介绍什么是 DCLP,这里稍作回顾.
线程安全的方式实现 Signleton 模式如下:
1 2 3 4 5 6 7 |
|
每次获取 Singleton 都要获取一个锁,但是实际上,我们只有当初始化 pInstance 时才需要一个锁。也就是只发生在第一次调用 instance 时。如果在一个程序运行时, instance 被调用了 n 次,我们只需要锁在第一次调用时。当我们知道那 n-1 次锁是没必要的.
DCLP 的关键点是发现,大多数 instance 的调用将看到 pInstance 是非空的,因此根本没必要去尝试初始化它。因此,DCLP 判断 pInstance 是否为空在尝试获取锁前。只有当判断成功( pInstance 还没有被初始化)才去获取锁,然后之后这个判断在此进行一次确保 pInstance 是仍然空的。(所以名字叫双重检查锁)。第二个检查是有必要的,因为从上可以看到,另外的线程可能碰巧初始化了 pInstance 在 pInstance 被第一次判断和获取锁之间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
单例模式(Singleton) 说明了各种不安全实现的缺陷,主要原因是 1) 编译器的乱序编译 和 2) CPU 的乱序执行指令.所以安全的实现依靠 memory barrier,防止它们的乱序,使得在多线程中得到同步,C++11 之前没有可移植的 C/C++函数,但现在,C++11 有了.
使用 C++11 的 Acqure 和 Release Fence
使用 Acqure 和 Release Fence 来实现它,并且保证对实例pInstance
进行原子操作,把它定义为atomic
类型,并用memory_order_relaxed
操作.(Relaxed
ordering: there are no synchronization or ordering constraints, only
atomicity is required of this operation.)如下实现代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
在多核系统中,这整个代码也是稳健的,因为 memory fences 在多个线程间建立了同步的关系.Singleton::m_pInstance
作为 guard variable,singleton 变量自身成为 payload.
如果没有这层同步关系的话,就不能保证第一个线程的所有写操作(这里就是
singleton 实力的创建)被第二个线程读取到,即使m_pInstance
已经被第二个线程能看到.
使用 C++11 的底层的内存顺序约束在 C++11 中也可以在单元操作时附加底层的内存顺序约束来达到同样的目的.一个
write-release 能同步于一个 read-release.
-
memory_order_acquire
: A load operation with this memory order performs the acquire operation on the affected memory location: prior writes made to other memory locations by the thread that did the release become visible in this thread. -
memory_order_release
: A store operation with this memory order performs the release operation: prior writes to other memory locations become visible to the threads that do a consume or an acquire on the same location.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
从深层分析来看,这种形式的免锁机制的同步比上面单独 memory fences 来的约束更小.这种形式的操作只意味在这个操作周围防止内存乱序,而 memory fences 意味着在一块区域内防止内存乱序.更多细节参考 preshing 的
Acquire and Release Fences Don’t Work the Way You’d Expect
的分析.
## 使用 C++11 的 Sequentially-consistent ordering
C++11 还提供了其他的方法来写 lock-free 的代码.当在 atomic 操作函数中忽略
std::memory_order
参数项,那么默认值是std::memory_order_seq_cst
,使得所有原子参数成为
sequentically consistent(SC)
原子.通过 SC 原子性,整个算法保证 sequentically consistent 只要没有 data races.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
SC 的原子性可能更容易理解.权衡点就是它产生的机器代码没有之前做法的高效.比如如下是 Gcc 4.8.2 intle X64 对上面代码产生的机器代码,通过g++ -O2 -std=c++11 -S
.
因为使用了 SC 原子性,对m_pInstance
的存储实现使用了mfence
指令,起到一个在 X64 上的 full memory fence.这是个更严格的指令想对于 DCLP 在 X64 上的实际需求.一个普通的mov
足以胜任.但也无关紧要,因为mfence
指令也仅仅执行一次而已,就在创建 singleton 的实例的代码路径上.
More
使用 Preshing 的小型可移植的 lock-free 库,在没有 C++11 的支持下,使用它的 Mintomic Fences 实现 DCLP.
更多关于 C++11 的 multithreading 库的详解见之后的文章.