前言
编译器在进行优化的时候,可能为了效率而交换不相关的两条相邻指令的执行顺序。也就是指令重排,这也就引发了一些问题,下面就带大家看两个经典的问题。
单例模式
第一个例子来自单例模式的双加锁,下面是典型的双加锁的单例模式代码:
T* ptr = nullptr;T* GetInstance() {if (nullptr == ptr) {lock();if (nullptr == ptr) {ptr = new T;}unlock();}return ptr;
}
上面的代码看起来没问题,并且采用了双加锁,能减少锁的竞争。
我们知道 C++ 的 new 做了两件事:
- 调用 ::operator new 分配内存
- 调用构造函数
所以 ptr = new T
包含了三个步骤:
- 调用 ::operator new 分配内存
- 在内存的位置上调用构造函数
- 将内存的地址赋值给 ptr
在这三步中,2 和 3 的顺序是可以交换的。也就是说,有可能:有一个线程分配了内存并将地址赋值给 ptr 了,但还没有初始化该内存。另一线程检测 ptr 不为空,就直接拿去使用了,这时可能引起不可预料的结果。
通常情况下,可以调用 CPU 提供的一条指令来解决该情况,这指令被称为 barrier。一条 barrier 指令会阻止 CPU 将该指令交换到 barrier 之后,也不能将之后的指令交换到 barrier 之前。
#define barrier() __asm__ volaticle("lwsync")
T* ptr = nullptr;T* GetInstance() {if (nullptr == ptr) {lock();if (nullptr == ptr) {T* tmp = new T;barrier();ptr = tmp;}unlock();}return ptr;
}
智能指针
有时我们会采用智能指针来管理内存,防止我们忘记释放或在某些场景手动释放十分麻烦。
我们有如下代码:
processQgw(shared_ptr<Qgw>(new Qgw), test());
令人遗憾的是,上述代码仍可能产生内存泄漏,并难以察觉。
编译器在调用函数前,需要准备好参数,所以上面代码会做以下三件事:
- 调用 test
- 执行 new Qgw
- 调用 shared_ptr 构造函数
C++ 编译器会以什么次序完成这些事情呢?可以确定的是 new Qgw 一定在 调用 shared_ptr 之前完成,上述三件事也一定在调用 processQgw 之前完成。编译器可能以下列次序调用:
- 执行 new Qgw
- 调用 test
- 调用 shared_ptr 构造函数
如果 test 的调用导致异常,new Qgw 返回的指针就再也找不到了,也就引起了内存泄漏。因为我们还没将返回的指针置入智能指针,智能指针也就对这种情况无能为力了。
为避免这类问题:使用分离语句,分别写出创建 Qgw,将它置入一个智能指针内,然后再把智能指针传给 processQgw:
shared_ptr<Qgw> pq = new Qgw;
processQgw(pq, test());
上述解决方法之所以能行,是因为编译器对于「跨越语句的各项操作」没有重新排列的自由,只有在一条语句内它才拥有那个自由。