C++设计模式之单例模式(创建型)

Singleton是一个非常常用的设计模式。几乎所有稍大的程序都会用到它。所以构建一个线程安全,并且高效的singleton很重要。

一般来说需要满足如下要求:

1.安全:在多线程环境下必须是线程安全的[保证并发性的安全]。

2.性能:大量的反复被调用,考虑性能问题[不使用各种锁]。

3.通用:尽可能地适用各种平台以及后面出现的新平台[尽量使用语言标准库]。

4.简单:实现简单方便适用[简洁有效]。

 

1. 在C++11中实现

C++11标准在6.7.4节中规定:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

即:如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待完成该变量完成初始化。

从C++11开始,编译器已经保证了在多线程同时调用的情况下static只会被初始化一次。请阅读Static local variables

If multiple threads attempt to initialize the same static local variable concurrently, the initialization occurs exactly once (similar behavior can be obtained for arbitrary functions with std::call_once).

Note: usual implementations of this feature use variants of the double-checked locking pattern, which reduces runtime overhead for already-initialized local statics to a single non-atomic boolean comparison.

 

所以我们可以用这个特性来实现:

1.1 static实现

实现方式1:

#include <iostream>

template<typename T>
class Singleton
{
public:
    static T& getInstance()
    {
        static T value;
        return value;
    }

    ~Singleton() = default;    //注意析构函数是default
private:
    Singleton() = default;    //注意构造函数是default
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

int main() 
{
    auto s1 = Singleton<int>::getInstance();
    std::cout << &s1 << std::endl;

    auto s2 = Singleton<double>::getInstance();
    std::cout << &s2 << std::endl;

    return 0;
}

局部静态变量可以延迟对象的构造,等到第一次调用时才进行构造。C++11中静态变量的初始化时线程安全的。

或者通过函数的形式:

template<typename T>
T& Singleton()
{
    static T instance;
    return instance;
}

1.2 std::call_once实现

在C++11中提供一种方法,使得函数可以线程安全的只调用一次。在C++11中提供了很方便的辅助类std::call_oncestd::once_flagstd::call_once是一种懒汉模式[lazy load的]很简单易用的机制。
     以下是对std::call_once的原文说明:
         rom:std::call_once@cplusplus.com 
         Calls fn passing args as arguments, unless another thread has already executed (or is currently executing) a call to call_once with the same flag. 
        If another thread is already actively executing a call to call_once with the same flag, it causes a passive execution: Passive executions do not call fn but do not return until the active execution itself has returned, and all visible side effects are synchronized at that point among all concurrent calls to this function with the same flag. 
If an active call to call_once ends by throwing an exception (which is propagated to its calling thread) and passive executions exist, one is selected among these passive executions, and called to be the new active call instead. 
         Note that once an active execution has returned, all current passive executions and future calls to call_once (with the same flag) also return without becoming active executions. 
       The active execution uses decay copies of the lvalue or rvalue references of fn and args, ignoring the value returned by fn.
大意就是:
     call_one保证函数fn只被执行一次,如果有多个线程同时执行函数fn调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态)—不会直接返回,直到活动线程对fn调用结束才返回。对于所有调用函数fn的并发线程的数据可见性都是同步的(一致的)。 
     如果活动线程在执行fn时抛出异常,则会从处于”passive execution”状态的线程中挑一个线程成为活动线程继续执行fn,依此类推。 
    一旦活动线程返回,所有”passive execution”状态的线程也返回,不会成为活动线程。
     由上面的说明,我们可以确信call_once完全满足对多线程状态下对数据可见性的要求。

实现方式二:

#include <iostream>
#include <memory>
#include <mutex>

class Singleton 
{
public:
  static Singleton& GetInstance() 
{
    static std::once_flag s_flag;
    std::call_once(s_flag, [&]() 
    {
      instance_.reset(new Singleton);
    });

    return *instance_;
  }

  ~Singleton() = default;

  void PrintAddress() const 
  {
    std::cout << this << std::endl;
  }

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
};

std::unique_ptr<Singleton> Singleton::instance_;

int main() 
{
  Singleton& s1 = Singleton::GetInstance();
  s1.PrintAddress();

  Singleton& s2 = Singleton::GetInstance();
  s2.PrintAddress();

  return 0;
}

利用call_once再结合lambda表达式得到函数形式的:


Singleton& getInstance() 
{
    static std::once_flag oc;//用于call_once的局部静态变量
    static Singleton* m_instance = nullptr;
    std::call_once(oc, [&] {  m_instance = new Singleton();});
    return *m_instance;
}

 

1.3 pthread_once

在C++11之前的多线程编程环境下,static实现方式无法保证并发的正确性,我们可以linux提供的phread库去实现。尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,pthread_once是很适合用来实现线程安全单例。

例 2://资源释放后续完成

#include <iostream>
#include <pthread.h>

template<typename T>
class Singleton
{
public:
    static T& getInstance()
    {
        pthread_once(&ponce_, &Singleton::init);
        return *value_;
    }

    static void init()
    {
        value_ = new T();
    }
    
private:
    Singleton(){}
    ~Singleton(){}
    Singleton(const Singleton&){}
    Singleton& operator=(const Singleton&) {}
	
    static pthread_once_t ponce_;
    static T* value_;
};

template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = NULL;

int main()
{
    int s = Singleton<int>::getInstance();

	return 0;
}

 

1.4 atomic_thread_fence

关于memory fence,不同的CPU,不同的编译器有不同的实现方式,要是直接使用还真是麻烦,不过,c++11中对这一概念进行了抽象,提供了方便的使用方式
    在c++11中,可以获取(acquire/consume)和释放(release)内存栅栏来实现上述功能。使用c++11中的atomic类型来包装m_instance指针,这使得对m_instance的操作是一个原子操作。下面的代码演示了如何使用内存栅栏[注:本文摘自:X-Programer的用C++实现单例模式4——C++11实现,后续进一步完善。

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
 
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);  
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release); 
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

上面的代码中atomic_thread_fence在创建对象线程和使用对象线程之间建立了一种“同步-与”的关系(synchronizes-with)。
      以下是摘自cplusplus关于atomic_thread_fence函数的说明:
           Establishes a multi-thread fence: The point of call to this function becomes either an acquire or a release synchronization point (or both). 
          All visible side effects from the releasing thread that happen before the call to this function are synchronized to also happen before the call this function in the acquiring thread. 
          Calling this function has the same effects as a load or store atomic operation, but without involving an atomic value

    中文大意是创建一个多线程栅栏,调用该函数的位置成为一个(acquire或release或两者)的同步点, 
    在release线程中此同步点之前的数据更新都将同步于acquire 线程的同步点之前,这就实现线程可见性一致。

 

上面atomic_thread_fence演示的代码使用内存栅栏锁定技术可以很方便地实现双重检查锁定。但是看着实在有点麻烦,在C++11中更好的实现方式是直接使用原子操作.

1.5 atomic

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
 
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            m_instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

如果你对memory_order的概念还是不太清楚,那么就使用C++顺序一致的原子操作,所有std::atomic的操作如果不带参数默认都是std::memory_order_seq_cst,即顺序的原子操作(sequentially consistent),简称SC,使用(SC)原子操作库,整个函数执行指令都将保证顺序执行,这是一种最保守的内存模型策略。
    下面的代码就是使用SC原子操作实现双重检查锁定。

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
 
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load();
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load();
        if (tmp == nullptr) {
            tmp = new Singleton;
            m_instance.store(tmp);
        }
    }
    return tmp;
}

 

 

1.6 通用的单例模式

在c++11之前,我们写单例模式的时候会遇到一个问题,就是多种类型的单例可能需要创建多个类型的单例,主要是因为创建单例对象的构造函数无法统一,各个类型的形参不尽相同,导致我们不容易做一个所有类型都通用的单例。现在c+11帮助我们解决了这个问题,解决这个问题靠的是c++11的可变模板参数。注本示例讲解的是通用类型的单例模式的实现,在多线程环境下是不安全的,不过我们先看看作者的实现。【摘自qicosmos的文章c++11改进我们的模式之改进单例模式

1.6.1 单个类型的实现

#include <iostream>
#include <string>

template <typename T>
class Singleton
{
public:
    template<typename... Args>
    static T* Instance(Args&& ... args)
    {
        if (m_pInstance == nullptr)
            m_pInstance = new T(std::forward<Args>(args)...);
        return m_pInstance;
    }
    static T* GetInstance()
    {
        if (m_pInstance == nullptr)
            throw std::logic_error("the instance is not init, please initialize the instance first");
        return m_pInstance;
    }
    static void DestroyInstance()
    {
        delete m_pInstance;
        m_pInstance = nullptr;
    }

private:
    Singleton(void) = default;
    virtual ~Singleton(void) = default;
    Singleton(const Singleton&) = default;
    Singleton& operator = (const Singleton&) = default;
private:
    static T* m_pInstance;
};

template <class T> T* Singleton<T>::m_pInstance = nullptr;

struct A
{
    A(const std::string&) { std::cout << "lvalue" << std::endl; }
    A(std::string&& x) { std::cout << "rvalue" << std::endl; }
};

struct B
{
    B(const std::string&) { std::cout << "lvalue" << std::endl; }
    B(std::string&& x) { std::cout << "rvalue" << std::endl; }
};

struct C
{
    C(int x, double y) {}
    void Fun() { std::cout << "test" << std::endl; }
};

int main()
{
    std::string str = "bb";
    // 创建A类型的单例
    Singleton<A>::Instance(str);
    // 创建B类型的单例,临时变量str被move之后变成右值,然后可以根据移动语义来避免复制
    Singleton<B>::Instance(move(str));
    // 创建C类型的单例,含两个参数
    Singleton<C>::Instance(1, 3.14);
    // 获取单例并调用单例对象的方法
    Singleton<C>::GetInstance()->Fun();
    // 释放单例
    Singleton<A>::DestroyInstance();
    Singleton<B>::DestroyInstance();
    Singleton<C>::DestroyInstance();

    return 0;
}

    这个单例模式可以解决不同类型构造函数形参不尽相同的问题,真正意义上对所有类型都通用的单例模式。初始化和取值分为两个接口,单例的用法为:先初始化,后面取值,如果中途销毁单例的话,需要重新取值。如果没有初始化就取值则会抛出一个异常。

1.6.2 Multiton的实现

#include <map>
#include <string>
#include <memory>
using namespace std;

template < typename T, typename K = string>
class Multiton
{
public:
    template<typename... Args>
    static std::shared_ptr<T> Instance(const K& key, Args&&... args)
    {
        return GetInstance(key, std::forward<Args>(args)...);
    }

    template<typename... Args>
    static std::shared_ptr<T> Instance(K&& key, Args&&... args)
    {
        return GetInstance(key, std::forward<Args>(args)...);
    }
private:
    template<typename Key, typename... Args>
    static std::shared_ptr<T> GetInstance(Key&& key, Args&&...args)
    {
        std::shared_ptr<T> instance = nullptr;
        auto it = m_map.find(key);
        if (it == m_map.end())
        {
            instance = std::make_shared<T>(std::forward<Args>(args)...);
            m_map.emplace(key, instance);
        }
        else
        {
            instance = it->second;
        }

        return instance;
    }

private:
    Multiton(void);
    virtual ~Multiton(void);
    Multiton(const Multiton&);
    Multiton& operator = (const Multiton&);
private:
    static map<K, std::shared_ptr<T>> m_map;
};

template <typename T, typename K>
map<K, std::shared_ptr<T>> Multiton<T, K>::m_map;

1.7 单例模式继承

有时候可能需要生成多个单例模式,如果写一个单例模式让其他的子类继承这个单例模式就可以代码复用了。让子类的构造函数为private或者protected,不让其创建对象。

方法:通过模板递归方法,把基类声明为派生类的友元,基类可以访问派生类的private和protected构造函数、数据和方法。

#include <iostream>

template<class T> 
class Singleton 
{
protected:
	Singleton() = default;  //protected确保子类可以创建对象
	virtual ~Singleton() = default;//确保子类析构可以调用派生类析构函数
	Singleton(const Singleton&) = delete;//保证外部不能创建实例
	Singleton& operator=(const Singleton&) = delete; //保证不能通过赋值操作创建实例
public:
	static T& getInstance()//返回对象引用而不用指针,避免被间接delete
	{
		static T theInstance;//利用C++11中static局部变量多线程下只初始化一次的特性(多线程下安全)
		return theInstance;  不需要用if来判断是否创建
	}
};

// A sample class to be made into a Singleton
class DerivedSingleton : public Singleton<DerivedSingleton>
{
	int x;
protected:
	friend class Singleton<DerivedSingleton>; //声明友元,保证可访问派生类private/protected成员变量,protected构造函数等 
	DerivedSingleton() { x = 0; } //protected:不可以创建实例
public:
	void setValue(int n) { x = n; }
	int getValue() const { return x; }
};

int main()
{
	DerivedSingleton& m = DerivedSingleton::getInstance();//只能创建一个实例

	std::cout << m.getValue() << std::endl;
	m.setValue(10);
	std::cout << m.getValue() << std::endl;

	DerivedSingleton& n = DerivedSingleton::getInstance();//m和n是同一个实例
	n.setValue(20);
	std::cout << n.getValue() << std::endl;
	
	std::cout << m.getValue() << std::endl;

	return 0;
}

结果如下:

 这里唯一不好的一点是,在每个子类中都需要声明基类是友元。有一种方法CRTP可以解决这个问题,请阅读不用虚函数机制,通过基类访问派生类private/protected数据和成员

1.8 通用的多线程安全单例模式

后续有时间了把这个代码和支持多线程的一起实现下

其他的一些单例模式方法都是老生常谈,而且也没有很好的解决问题,自己可以搜下。对于比较典型的一种方法Double-Checked Locking Pattern (DCLP)实际上也是存在严重的线程安全问题。Scott Meyers and 和Alexandrescu写的一篇文章里面专门分析了这种解决方案的问题C++ an the Perils of Double-Checked Locking

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值