设计模式学习(一)单例模式补充——指令重排
前言
在《单例模式学习》 中曾提到懒汉式DCLP的单例模式实际也不是线程安全的,这是编译器的指令重排导致的,本文就简单讨论一下指令重排对单例模式的影响,以及对应的解决方法。
指令重排简介
指令重排(Instruction Reordering)是编译器或处理器为了优化程序执行效率而对程序中的指令序列进行重新排序的过程。这种重排可以发生在编译时也可以发生在运行时,目的是为了减少指令的等待时间和提高执行的并行性。
指令重排可能会引入并发程序中的一些问题,特别是在多线程环境中,没有适当同步机制的情况下,可能会导致程序的执行结果不符合预期。
下面介绍指令重排在单例模式中的影响
指令重排对单例模式的影响
首先回顾一下懒汉式DCLP单例模式的代码
1 | class CSingleton |
注意这一句:
1 | instance = new CSingleton(); //并非一个原子操作,不是可重入函数 |
instance
的初始化其实做了三个事情:
- ①内存分配:为CSingleton对象分配一片内存
- ②对象构造:调用构造函数构造一个CSingleton对象,存入已分配的内存区
- ③地址绑定:将指针instance指向这片内存区(执行完这步instance才是非 nullptr)
但是由于指令重排,编译器会将顺序改变为:
1 | instance = //步骤三 |
现在考虑以下场景:
1.线程A进入getInstance(),判断instance为空,请求加锁,然后执行步骤一和三组成的语句,之后A被挂起。此时instance为非空指针(指向了一块内存),但instance指向内存里面的CSingleton对象还未被构造出来。
2.线程B进入getInstance(),判断instance非空(因为在A线程中instance已经为非空指针了),直接返回instance。之后用户使用该指针访问CSingleton对象,嘿!您猜怎么着,这个CSingleton对象还没被构造出来呢。
总的来说,只有步骤一和二在三前面执行,DCLP才有效
改进方法
std::call_once和std::once_flag
std::call_once
配合std::once_flag
确保了instance = new CSingleton()
只会被执行一次,无论它被多少个线程访问。这避免了指令重排在多线程下导致的问题。
1 | class CSingleton |
std::atomic和内存顺序
1 | class CSingleton |
局部静态变量
最后,害得是局部静态变量形式的单例模式,大道至简!
1 | static CSingleton& getInstance() |
具体原因见:《单例模式学习》
总结
本文讨论了指令重排对多线程下的单例模式的影响,并例举了几个解决方案。后面可能还会更新别的解决方案
参考文章
1.C++ and the Perils of Double-Checked Locking
2.Double-Checked Locking is Fixed In C++11
- 标题: 设计模式学习(一)单例模式补充——指令重排
- 作者: paw5zx
- 创建于 : 2024-03-19 00:42:00
- 更新于 : 2024-08-22 13:50:51
- 链接: https://paw5zx.github.io/singleton-reorder/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。