https://blog.csdn.net/janeqi1987/article/details/76147312
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
cout <<"cons" << endl;
}
public:
static CSingleton *GetInstance()
{
static CSingleton m_pInstance;
return &m_pInstance;
}
void showmsg()
{
cout << "hello" << endl;
}
};
if(1)
{
CSingleton *obj2 = CSingleton::GetInstance();
obj->showmsg();
CSingleton *obj = CSingleton::GetInstance();
obj->showmsg();
}
output:
cons
hello
hello
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
cout <<"cons" << endl;
}
static CSingleton *m_pInstance;
public:
static CSingleton *GetInstance()
{
if (m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
void showmsg()
{
cout << "hello" << endl;
}
};
if(1)
{
CSingleton *obj1 = CSingleton::GetInstance();
obj1->showmsg();
CSingleton *obj2 = CSingleton::GetInstance();
obj2->showmsg();
}
output:
cons
hello
hello
Singleton是一个非常常用的设计模式。几乎所有稍大的程序都会用到它。所以构建一个线程安全,并且高效的singleton很重要。既然要讨论这个主题,我们就先来定义一下我们的需求:
- Lazy initialization。只有在第一次使用的时候才需要初始化出一个singleton对象。这使得程序不需要考虑过多顺序耦合的情况。同时也避免了启动时初始化太多不知道什么时候才会用到的东西。
- 线程安全。多个线程有可能同时调用singleton。如果只需要单线程,那实在没什么需要讨论的。
- 高效。因为singleton会被反复调用,如果效率低的话浪费太大了。
- 通用。适合现有的各种平台,以及未来可能出现的平台。
有了这些需求,我们就可以开始讨论如何构造这么一个singleton。下面以C++为基础来解析这个问题。
原始版本
在《设计模式》一书中,给出了singleton的基本结构:
T& Singleton()
{
static T instance;
return instance;
}
这个实现是Lazy initialization(需求1),并且高效(需求3)和通用(需求4)。但不是线程安全的(需求2)。因为在C++98中,并没有任何关于多线程的概念。所以也并没有定义如果多个线程同时初始化一个static局部变量会出现什么。
改进1:加锁
最简单的线程安全改进就是加个锁:
std::mutex m;
T& Singleton()
{
std::unique_lock lock(m);
static T instance;
return instance;
}
这个实现是Lazy initialization(需求1),并且是线程安全(需求2)和通用(需求4),但它并不高效(需求3)。因为这不但有lock/unlock的开销,还相当于把所有的调用都串行化了。对于频繁调用的情况来说不是个好事。
改进2:双重检查
一个广为人知的改进就是双重检查:
T* instance = nullptr;
std::mutex m;
T& Singleton()
{
if (nullptr == instance)
{
std::unique_lock lock(m);
if (nullptr == instance)
{
instance = new T;
}
}
return *instance;
}
这样的双重检查是lazy initialization(需求1),并比上一个实现提高了效率(需求3),因为除了第一次调用的时候需要加锁之外,其他都可以并行判断和返回。但是,这么做真的能线程安全吗(需求2)?这么做真的通用吗(需求2)?
在现代的编译器和CPU上,为了提高性能,真正的执行顺序和高级语言中的已经不一致了。双重检查也不例外。编译器生成的机器码会对内存读写指令进行重新排序,CPU的乱序执行会进一步改变内存读写指令的执行顺序。因为有了这些乱序的存在,instance = new T这一行并不能一次执行完。比如,instance = new T可以分解成3个操作,申请内存、赋值给instance、调用构造函数。后两个操作是以什么方式进行的,只有编译器能决定。如果先赋值给是,再调用构造函数,那么另一个线程同时调用了Singleton()的话,就会发现instance已经有值了,直接使用。但因为这时候还没有在instance上调用构造函数,对象还没被建立出来,这样就会引发严重的问题。
改进3:加上barrier
VC提供了一些intrinsic,以提示编译器和CPU内存读写的顺序。用了这些intrinsic之后,就能保证编译器和CPU不会制造这些混乱。
T* instance = nullptr;
std::mutex m;
T& Singleton()
{
if (nullptr == instance)
{
std::unique_lock lock(m);
if (nullptr == instance)
{
T* tmp = new T;
MemoryBarrier();
instance = tmp;
}
}
return *instance;
}
有了MemoryBarrier(),就能保证到instance = tmp那行的时候,前面的new T一定已经完成。这样就是线程安全的了(需求2)。同时它也有双重检查的有点,lazy initialization(需求1)和高效(需求3)。那么,通用(需求4)吗?
很遗憾的是,在现有的x86、x64和ARM的内存模型上,这么做是可以的。但在某些很弱的内存模型上,比如已死多时的Alpha,这么做照样不行。未来的平台仍可能出现这样的弱内存模型,所以不得不防。在Alpha上,instance的值被放入寄存器之后,即便它后来被另一个线程赋值了,CPU也不打算再次读取这个地址。所以第二个if仍会通过,以至于new T再次被执行,于是singleton不再是singleton。
有人会问,volatile似乎就是为了解决这样的问题的啊,是不是在这里声明成volatile T* instance就行了?确实,volatile是让编译器生成代码的时候不做假设优化,所以两次读取instance会真的让编译器生成2次读取指令。但Alpha这种情况是CPU偷懒了,即便编译器让它读两次,它都不会就范。所以volatile无法解决这个问题。当然,应该还可以用Alpha的特殊指令来强制CPU再次读取某个变量,但这么做就会让程序不在通用。另外,volatile最多也就是保证那个变量自身不会被错误假设,但无法保证变量之间的读写顺序。尤其是,volatile变量和非volatile变量之间的读写顺序是完全无保证的。
改进4:atomic
Boost引入了atomic库,同时这也被放入C++11的标准库中。它不但提供了原子增减等计算,并且在实现上要求能保证编译器生成代码的读写顺序和CPU执行的读写顺序。也就是说,可以简单地用atomic来实现双重检查singleton。下面用到C++11的代码也可以用boost替换。
std::atomic<T*> instance;
std::mutex m;
T& Singleton()
{
if (nullptr == instance)
{
std::unique_lock lock(m);
if (nullptr == instance)
{
instance = new T;
}
}
return *instance;
}
反而变简单了,所有barrier的细节都放到了atomic里,又编译器附带的库来实现。这是第一个能满足上述4个需求的singleton实现。
就到此为止了?不。atomic在判断上还是有开销的,就这么直接使用会让那几行语句都顺序执行。前面提到了,编译器和CPU都倾向于乱序执行,为的是提高效率。都强制了顺序结果会没那么高效了。所以我们还应该能进一步改进。
改进5:更精确地控制
atomic的那些重载操作符因为没法知道用户会如何调用它们,所以只能做非常粗略的假设,也就是在前后都加上了barrier。实际上我们这里需要的只是一个方向的barrier(第一个if那行,读取instance之后加一个读barrier;以及new之后加一个写barrier),而不是两个方向。好在atomic提供了一套精确控制barrier方向的方法,可以用来改进这个问题。
std::atomic<T*> instance;
std::mutex m;
T& Singleton()
{
T* tmp = instance.load(std::memory_order_consume);
if (nullptr == tmp)
{
std::unique_lock lock(m);
tmp = instance.load(std::memory_order_relaxed);
if (nullptr == tmp)
{
instance.store(new X(), std::memory_order_release);
}
}
return *instance;
}
consume表示后面的读写不会被重排到这次load之前。release表示前面的读写不会被重排到这次store之后。relaxed表示无所谓顺序。这样就保证了在barrier最少的情况下达到目的。当然,这么做的前提仍然是库的实现要对。
改进6:返朴归真
因为编译器的原因,把singleton搞得这么复杂。能不能让编译器做点什么呢?能!C++11标准中特别提到了这种状况:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
换句话说,在多线程同时调用的情况下static只会被初始化一次。也就是说,对于一个符合这个要求的C++11编译器来说,只需要基本结构就可以了。
T& Singleton()
{
static T instance;
return instance;
}
是不是有一种返朴归真的感觉?因为编译器保证了static的行为,这里就完全不用担心多线程带来的内存泄漏或重复初始化。所以也不必要用那套反复的方法保证顺序了。当然,在编译器里用的就是全面提到的方法来保证static的行为。
目前实现了这个要求的C++编译器有,VC14(VS2015)和GCC 4.0以上。其他编译器暂时没查到资料。所以安全起见,这里最好用一个#ifdef,对于支持新static的用上改进6的方法,否则用改进5的方法。
总结
好了,目前为止我们得到了两个满足几乎所有singleton需求的实现。从此不必再拘泥于各种强制顺序、低效、不安全、平台相关的singleton了。
另外一篇文章:
想必每一位程序员都对设计模式中的单例模式非常的熟悉吧,以往我们用C++实现一个单例模式需要写以下代码:
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
static CSingleton *m_pInstance;
public:
static CSingleton * GetInstance()
{
if (m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
};
CSingleton *CSingleton::m_pInstance=NULL;
当然,这份代码在单线程环境下是正确无误的,但是当拿到多线程环境下时这份代码就会出现race condition,因此为了能在多线程环境下实现单例模式,我们首先想到的是利用同步机制来正确的保护我们的shared data,于是在多线程环境下单例模式代码就变成了下面这样:
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
static CSingleton *m_pInstance;
mutex mtx;
public:
static CSingleton * GetInstance()
{
mtx.lock();
if (m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
mtx.unlock();
return m_pInstance;
}
};
CSingleton *CSingleton::m_pInstance=NULL;
正确是正确了,问题是每次调用GetInstance函数都要进入临界区,尤其是在heavy contention情况下函数将会成为系统的性能瓶颈,我们伟大的程序员发现我们不必每次调用GetInstance函数时都去获取锁,只是在第一次new这个实例的时候才需要同步,所以伟大的程序员们发明了著名的DCL技法,即Double Check Lock,代码如下:
Widget* Widget::pInstance{ nullptr };
Widget* Widget::Instance()
{
if (pInstance == nullptr)
{
// 1: first check
lock_guard<mutex> lock{ mutW };
if (pInstance == nullptr)
{
// 2: second check
pInstance = new Widget();
}
}
return pInstance;
}
曾今有一段时间,这段代码是被认为正确无误的,但是一群伟大的程序员们发现了其中的bug!并且联名上书表示这份代码是错误的。要解释其中为什么出现了错误,需要读者十分的熟悉memory model,这里我就不详细的说明了,一句话就是在这份代码中第三行代码:if (pInstance == nullptr)和第六行代码pInstance = new Widget(); 没有正确的同步,在某种情况下会出现new返回了地址赋值给pInstance变量而Widget此时还没有构造完全,当另一个线程随后运行到第三行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。在C++11没有出来的时候,只能靠插入两个memory barrier来解决这个错误,但是C++11已经出现了好几年了,其中我认为最重要的是引进了memory model,从此C++11也能识别线程这个概念了!
因此,在有了C++11后我们就可以正确的跨平台的实现DCL模式了,代码如下:
atomic<Widget*> Widget::pInstance{ nullptr };
Widget* Widget::Instance()
{
if (pInstance == nullptr)
{
lock_guard<mutex> lock{ mutW };
if (pInstance == nullptr)
{
pInstance = new Widget();
}
}
return pInstance;
}
C++11中的atomic类的默认memory_order_seq_cst保证了3、6行代码的正确同步,由于上面的atomic需要一些性能上的损失,因此我们可以写一个优化的版本:
atomic<Widget*> Widget::pInstance{ nullptr };
Widget* Widget::Instance()
{
Widget* p = pInstance;
if (p == nullptr)
{
lock_guard<mutex> lock{ mutW };
if ((p = pInstance) == nullptr)
{
pInstance = p = new Widget();
}
}
return p;
}
但是,C++委员会考虑到单例模式的广泛应用,所以提供了一个更加方便的组件来完成相同的功能:
static unique_ptr<widget> widget::instance;
static std::once_flag widget::create;
widget& widget::get_instance()
{
std::call_once(create, [=]{ instance = make_unique<widget>(); });
return instance;
}
可以看出上面的代码相比较之前的示例代码来说已经相当的简洁了,但是!!!有是但是!!!!在C++memory model中对static local variable,说道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.因此,我们将会得到一份最简洁也是效率最高的单例模式的C++11实现:
widget& widget::get_instance() {
static widget instance;
return instance;
}
用Herb Sutter的话来说这份代码实现是“Best of All”的。