C++单例模式——并非你想的那像简单

Author:WenHui,WuHan University,2012-12-10

前段时间忙着找工作,有一次面试官让我用C++写单例模式,我刷刷刷提笔就来。于是就随手写了如下的代码v1版

 1 class Singlon
 2 { 
 3 private:
 4     static Singlon * inst;
 5 
 6 public:      
 7     static Singlon& instance()
 8     {
 9         if(inst != NULL) 
10         {
11             return *inst;
12         }
13         else 
14         {
15             inst = new Singlon();             
16             return *inst;
17         }
18     } // instance
19 }; // class Singlon
20 
21 Singlon* Singlon::inst = NULL;

v1是一份很标准的单例模式代码,但不够实用。面试官看着我,说“你再好好想想...”

我原本很自负,结果被问傻了,紧张之下依稀想起《程序员面试宝典C++版》的内容,好像没考虑多线程问题。于是绞尽脑汁地回想C++中的线程互斥,还好记起点LINUX C下互斥锁,于是乎就有了改进的v2版

 1 class Singlon
 2 { 
 3 private:
 4     static Singlon * inst;
 5     pthread_mutex_t inst_lock;
 6 
 7 public:
 8     static Singlon& instance()
 9     {
10         if(inst != NULL)
11         {
12             return *inst;
13         }
14         else 
15         {
16             pthread_mutex_lock(&inst_lock);
17             if(inst == NULL)
18             {
19                 inst = new Singlon();
20             } // if
21             pthread_mutex_unlock(&inst_lock);
22             return *inst;
23         } // else
24     } // instance
25 }; // class Singlon
26 
27 Singlon* Singlon::inst = NULL;
28 Singlon::inst_lock = PTHREAD_MUTEX_INITIALIZER;

v2中首先直接判断inst是否为NULL,若不为NULL则避免调用互斥锁,因为互斥锁不仅使用系统调用,而且将导致多线程竞争等待,产生睡眠。

当写完v2时,终于有种如释重负的感觉,很欣慰,可是面试官冷冷地问我:“你仔细看看,还有什么问题么?”当时脑子一懵,TMD的还有问题?fuck!仔细再check了一下,发现if语句确实还可以有优化的余地,于是胆战心惊地写下了v3版:

 1 class Singlon
 2 { 
 3 private:
 4     static Singlon * inst;
 5     pthread_mutex_t inst_lock;
 6 
 7 public:
 8     static Singlon& instance()
 9     {
10         if(likely(inst != NULL)) 
11         {
12             return *inst;
13         }
14         else 
15         {
16             pthread_mutex_lock(&inst_lock);
17             if(likely(inst != NULL))
18             {
19                 pthread_mutex_unlock(&inst_lock);
20                 return *inst;
21             } // if
22             else 
23             {
24                 inst = new Singlon();
25                 pthread_mutex_unlock(&inst_lock);
26                 return *inst;
27             } // else
28         } // else
29     } // instance
30 }; // class Singlon
31 
32 Singlon* Singlon::inst = NULL;
33 Singlon::inst_lock = PTHREAD_MUTEX_INITIALIZER;

v3由于互拆锁会导致效用问题,越尽早释放越好,所以将一条if语句拆成两条、迟早释放,并且将条件概率大的情况放到if而非else中,用likely在汇编层上优化。我感觉这次大体上应该符合面试官要求了,但是面试官继续追问,“我再给你一次机会好好想想……”当时正值午餐时间,早晨没吃饭,肚子饿得咕咕叫,脑子都快疯了。幸好最后换了个思路,写下v4版:

 1 class Singlon
 2 { 
 3 private:
 4     static Singlon * inst;
 5 
 6 public:
 7     static Singlon& instance()
 8     {           
 9         return *inst;
10     } // instance
11 }; // class Singlon
12 
13 Singlon* Singlon::inst = new Singlon();

当提交给面试官看时,心想”今天真是倒霉”,幸而走屎运,v4预先创建好一个对象。虽然浪费空间,但不用再考虑TMD的线程互斥问题。面试官看了之后,语重心长地讲“你这个浪费空间太大,我要你实现初次使用时再申请。给你最后一次机会检查检查代码。”

晕!当时感觉已经能用的招都用了,后来放弃了,根他解释了下v3版代码,然后问他到底哪有问题。他说:“你为什么要使用互斥锁?”于是给他解释LINUX下互斥锁的实现机制。MutexLock使用SpinLock实现互斥,循环检测并休眠。当时突然想起可以直接用SpinLock实现v3的互斥,因为竞争仅发生一次,SpinLock直接采用循环检测不休眠的方式,使用“内存屏障”、“CPU屏障”等技术保证内存和CPU指令的“有序”。v5版如下: 

 1 class Singlon
 2 { 
 3 private:
 4     static Singlon * inst;
 5     static raw_spinlock_t inst_lock;
 6 
 7 public:
 8     static Singlon& instance()
 9     {
10         if(likely(inst != NULL)) 
11         {
12             return *inst;
13         }
14         else 
15         {
16             spin_lock(&inst_lock);
17             if(likely(inst != NULL))
18             {
19                 spin_unlock(&inst_lock);
20                 return *inst;
21             } // if
22             else 
23             {
24                 inst = new Singlon();
25                 spin_unlock(&inst_lock);
26                 return *inst;
27             } // else
28         } // else
29     } // instance
30 }; // class Singlon
31 
32 Singlon* Singlon::inst = NULL;
33 spin_lock_init(Singlon::inst_lock);

面试官看完v5版之后,问spin_lock的实现原理,由于看过LINUX内核怎么实现所以说得比较清楚。听完我的解释,当时已经中午一点多,他也没有再多说什么,终于结束这个该死的问题。其实C++没有像JAVA和C#那样从语法上提供同步互斥的机制,我中间也表示希望换成C#,但他拒绝了,因为自我简介时说精通C++。唉,~~~~~~

其实,v5版本还可以优化,例如采用无锁的办法。但总得说来,只是将spin_lock的代码简化了一下内嵌进来。不知道是否有哪位高人可以指点一二,给一个最终优化的C++版单例模式?


《无锁队列的实现》,http://www.kuqin.com/algorithm/20120907/330193.html

原文地址:https://www.cnblogs.com/icanth/p/2811855.html