在前些年,双重检验锁据说被广泛的运用。其形式如下:
#include <iostream>
#include <mutex>
using namespace std;
class SomeObject
{
public:
void Hello()
{
cout << "Hello" << endl;
}
};
class BadAtempt
{
SomeObject *someDetail;
mutex m;
public:
void init()
{
if (!someDetail)
{
lock_guard<mutex> lk(m);
if (!someDetail)
{
someDetail = new SomeObject;
}
}
someDetail->Hello();
}
};
首先要明确的是为什么要用双重检验锁:为了保护数据的初始化过程,光这么说你或许还不能了解其真正内涵,那咱们首先这么想,先不管多线程,在单进程下创建一个类的实例,BadAtempt a;
就行了,我们要明确的是这个实例中有个类型为someObject
的成员变量没有初始化。为什么要用指针,就是为了延迟初始化,因为或许你一辈子都不会在这个实例中使用这个成员变量,那又何必初始化它占用多余的空间。使用指针初始化,就需要考虑多线程所带来的问题了。
多线程是并发执行的,在同一时间对同一个实例,可能有多个线程同时初始化这个someObject
类型的成员变量,因为在它们看来,这个变量都还未初始化。
void init()
{
if (!someDetail)//两个线程同时通过了这个判断
{
someDetail = new SomeObject;//两个线程同时new,Exception
}
someDetail->Hello();
}
这当然就会导致异常了,那你说好,那我加个锁不就行了。
void init()
{
lock_guard<mutex> lk(m);//锁住,仅允许一个线程执行接下来的操作
if (!someDetail)
{
someDetail = new SomeObject;
}
someDetail->Hello();
}//释放锁,允许下个阻塞的线程上锁继续。
这确实行,但你不觉得慢么,会相当慢,因为这种情况下如果有100个线程同时初始化,第一个线程上锁,其他99个就得等。第一个结束了,第二个上锁,其他98个等……这叫做线程的序列化,多线程硬生生变成了好像单进程一样顺序执行。
所以应运而生了双重检验锁
void init()
{
if (!someDetail)//如果有两个以上的进程通过了这个判断
{
lock_guard<mutex> lk(m);//没有关系,在这锁住
if (!someDetail)//再判断
{
someDetail = new SomeObject;//仅有一个线程能初始化。
}
}//ok,释放锁,其他所有线程都不会再序列化的被阻塞。
someDetail->Hello();
}
看我的注释,你会感觉这种方式,哎哟不错哦。但它其实内涵一个恶劣的race condition。你要注意一个关键点,这个关键点也是避免序列化的关键:第一个判断没有上锁,你再注意第二个关键点:判断如果是false,接下来的语句是someDetail指针调用Hello函数。仔细想想,在多线程并发情况下,如果一个线程已经进行到了给指针赋予指向的对象的语句并且正在赋值还没赋值完(正在执行第8行),其他线程是可以进行第一个判断的因为第一个判断没有上锁,但是它判断的是false!它以为指针已经指向了正确的对象,然后它就继续调用Hello(),自然而然,未定义的情况就出现了。这确实是个不容易发现但很致命的race condition。
你说那怎么办?初始化后不直接调用指针?总不是个长久之计(甚至连个计都算不上)。
C++中提供了一个call_once()方法与once_flag变量助你一臂之力。**
class A
{
SomeObject *someDetail;
mutex m;
once_flag o; //整个once_flag
public:
void init()//整个初始化函数
{
someDetail = new SomeObject;//啥也不用担心,就像单进程那样就行
}
void someDetailHello()
{
call_once(o,&A::init);//调用这个函数,注意第二参数写法。
someDetail->Hello();
}
};
simple and easy.