我是靠谱客的博主 懦弱汽车,最近开发中收集的这篇文章主要介绍浅析C++单例模式的实现,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

单例模式可以说是老生常谈的面试考点。从最经典的单例模式实现开始,我们逐步提供了多种单例模式的实现方式,并一一分析它们的优缺点。

方案一:教科书经典方案

使用一个静态指针管理对象, 当第一次需要时创建此对象。

// 在此略去构造函数等无关代码,仅讨论单例模式本身
class Singleton{
public:
static Singleton& getInstance(){
if(instance_ == nullptr)
instance_ = new Singleton{};
return *instance_;
}
private:
static Singleton* instance_;
};

显然,这种经典的实现方式存在诸多问题:

  • 仅能单线程下正确运行,多线程时不保证对象唯一。
  • 需要手动释放申请的资源,以防内存泄漏。
  • 每次调用都存在一次判断的开销(几乎无法避免)。

首先,针对多线程正确申请资源这一点,可以使用DCL(Double check lock),即使用一个mutex保护资源的申请过程,并且通过两次判断避免争抢。这就引入了第二个方案。

方案二:DCL

双重锁定检查(DCL)在两次判断中进行一个加锁操作,首先第一次判断,确定了此时对象还未构造,故申请互斥锁并进入临界区完成对象构造。而如果发生争抢,也会被互斥量阻拦,随着构造完成,第二次检查时将发现对象已存在,保证了线程安全。

class Singleton {
public:
static Singleton& getInstance() {
if(instance_ == nullptr) {
mutex_.lock();
if(instance_ == nullptr){
instance_ = new Singleton();
}
mutex_.unlock();
}
return *instance_;
}
private:
static std::mutex mutex_;
static Singleton instance_;
};

这种方案通过牺牲一点微不足道的创建时间,来获得线程安全性保障。但是在C++中使用DCL其实是不安全的,这种不安全性是因为语句 instance_ = new Singleton(); 并不是一条原语,而是三个顺序调用

  1. 分配 Singleton 需要的内存

  2. 在内存上调用此对象的构造函数

  3. 将内存块的首地址赋予指针

C++编译器允许乱序执行这三条无关指令,指令执行的顺序不一定是1,2,3,也可能是1,3,2。在这种情况下,可能出现构造线程在3和2之间被挂起,而其他线程访问了此单例——线程安全性被破坏。

因此,即使使用DCL技术,也无法保证单例创建的线程安全,我们需要的是操作系统层面的保证。

方案三:std::call_once

在C++标准库中,为我们提供了 std::call_once 函数,此函数提供了在多线程下只进行一次调用的保证,这正是我们在单例模式中需要的。

class Singleton{
public:
static Singleton& getInstance(){
if(!instance_)
call_once(flag_, Singleton::create);
return *instance_
}
private:
static void create(){
instance_ = new Singleton();
}
static Singleton* instance_;
static std::once_flag flag_;
};

通过 std::call_once 调用,我们可以很轻松实现线程安全的单例模式。同样,POSIX标准库也要求了一个 pthread_once 调用,用于实现相同的功能。

到目前为止,我们还只解决了三个问题中的第一个,对于后两个问题,有一个简单的解决方案——静态局部变量。

方案四:静态局部变量

静态局部变量是指被 static 修饰的局部变量,只有当函数首次调用时,会创建这个变量,随后的函数调用都将沿用这个变量,这与单例模式的需要不谋而合。

class Singleton{
public:
static Singleton& getInstance(){
static Singleton instance{};
return instance;
}
};

此方案可以算得上优雅且简易的单例模式实现了。使用这个函数不会出现创建多个对象的问题,也不需要检查和释放资源。不过,依然存在一点点小问题——当多个线程同时调用此函数时,此对象也许会被多次初始化?好在C++11标准解决了局部变量初始化的线程安全问题,明确了一个对象只能初始化一次。

此外,这种申请方式在首次调用函数前,就已经完成了内存申请,并且在程序结束前,释放该内存。对于生命周期贯穿程序的变量来说,使用此方案并无不妥,但对于一些仅在特定时间使用的单例来说,也算是一种变相的内存泄漏了。

为了提供更灵活的内存控制方式,可以使用 std::shard_ptrstd::weak_ptr 来管理对象,实现内存的自动释放。

方案五:结合方案二和智能指针

在智能指针中,可以使用 std::make_shared 方法将内存分配和构造顺序化,这样就解决了DCL中的线程安全问题,随后引入智能指针管理内存,如没有线程使用此单例,则此单例自动析构。

class Singleton {
public:
static std::shared_ptr<Singleton> getInstance() {
if (instance_.expired()) {
std::lock_guard<std::mutex> lock(mutex_);
if (instance_.expired())
instance_ = std::make_shared<Singleton>();
}
return instance_.lock();
}
private:
static std::weak_ptr<Singleton> instance_;
static std::mutex mutex_;
};

当然,这里也存在几个实现细节问题。首先是因为使用了智能指针,因此构造函数和析构函数都必须是public的,或者为其提供特定的deleter,无论如何都需要暴露接口。其次,这种实现可能导致对象的频繁创建和释放,并且不能记录状态,有悖单例模式的应用场景。最后,没能解决每次调用都会发生的存在检查环节。

总结

在C++中,想要正确实现单例模式,还是很困难的。一点不注意就会引起线程安全问题。对于使用C++11及以后新标准的实现,可以采用方案四,对于未更新编译器的情况,则只能采用DCL。

最后

以上就是懦弱汽车为你收集整理的浅析C++单例模式的实现的全部内容,希望文章能够帮你解决浅析C++单例模式的实现所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(32)

评论列表共有 0 条评论

立即
投稿
返回
顶部