常用设计模式(单件模式-工厂模式demo)-c++版

1、定义

保证一个类仅有一个实例,并提供该实例的全局访问点。

2、示例

原始的单例模式
单例模式要做如下事情:

不能通过构造函数构造,否则就能够实例化多个。构造函数需要私有声明
保证只能产生一个实例
下面是一个简单的实现:

class Singleton
{
  private:
    static Singleton *instance;
    Singleton(){};

  public:
    static Singleton *getInstance()
    {
        if (instance== nullptr)
        {
            instance= new Singleton();
        }
        return instance;
    }
};

Singleton * Singleton::instance= nullptr;

int main()
{
    Singleton * s = Singleton::getInstance();
    return 0;
}

使用局部静态对象来解决存在的两个问题
刚刚的代码中有两个问题,一个是多线程的情况下可能会出现new两次的情况。另外一个是程序退出后没有运行析构函数。
下面采用了静态对象来解决。
在C++11标准中,要求局部静态变量初始化具有线程安全性,推荐使用static局部对象。
另外还有一个版本的懒汉模式代码,也是支持线程安全(打开编译器C++11支持):

class Singleton
{
  private:
    static Singleton *instance;
    Singleton(){cout << "构造" << endl;};
    ~Singleton(){cout << "析构" << endl;}

  public:
    static Singleton *getInstance()
    {
        static Singleton s;
        return &s;
    }
};


int main()
{
    cout << "单例模式访问第一次前" << endl;
    Singleton * s = Singleton::getInstance();
    cout << "单例模式访问第一次后" << endl;
    cout << "单例模式访问第二次前" << endl;
    Singleton * s2 = Singleton::getInstance();
    cout << "单例模式访问第二次后" << endl;
    return 0;
}

在这里插入图片描述
该代码可能在c++11之前的版本导致多次构造函数的调用,所以只能在较新的编译器上使用。
所以,c++11之前的版本,静态对象线程会不安全

使用mutex以及静态成员来析构单例。
该方案的劣处在于锁导致速度慢,效率低。但是至少是正确的,也能在c++11之前的版本使用,代码的示例如下:

//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
    Lock lock;
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

使用双锁检查导致未初始化的内存访问

使用如下的代码来实现已经初始化的对象的直接返回。可以使上述代码性能会大大加快。

由于CPU乱序执行,可能导致访问到未经初始化的对象的引用,导致未定义行为导致段错误。

https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

假如线程A进入锁内并分配对象的空间,但是由于指令可能乱序,实际上导致instance被先指向一块未被分配的内存,然后再在这块内存上进程初始化。但是在指向后,未初始化前,另一线程B可能通过getInstance获取到这个指针。说白了,实际上m_instance = new Singleton();做了三件事,第一件事申请一块内存,第二件事调用构造函数,第三件是将该内存地址赋给instance_。但是不同的编译器表现是不一样的。可能先将该内存地址赋给m_instance ,然后再调用构造函数。这是线程A恰好申请完成内存,并且将内存地址赋给m_instance ,但是还没调用构造函数的时候。线程B执行到第一个if,判断m_instance 此时不为空,则返回该变量,然后调用该对象的函数,但是该对象还没有进行构造。

//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
    
    if(m_instance==nullptr){
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
    }
    return m_instance;
}

乍看这种代码是没有问题的,但是问题的来源是CPU的乱序执行,C++的New操作实际上包含了两个步骤:

1、分配内存
2、调用构造函数
所以准确来说为三个步骤
1、分配内存
2、在内存的位置上调用构造函数
3、将内存的地址赋值给pInst
因为(2)和(3)是可以颠倒的,所以可以出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。如果另外一个线程对GetInstance的调用,此时第一个if为false,这样就会返回一个未构造完成的对象,此时可能会导致程序崩溃。

解决思路

许多体系结构都提供barrier指令,POWERPC提供了其中一条名为lwsync的指令,我们可以这样来保证线程安全:

#define barrier() __asm__ volatile ("lwsyc")
volatile T* pInst = 0;
T* GetInstance()
{
	if (!pInst) 
	{
		lock();
		if (!pInst)
		{
			T* temp = new T;
			barrier()
			pInst = temp;
		}
		unlock();
	}
	return pInst;
}

https://blog.csdn.net/bobodem/article/details/82860856

尝试使用局部变量并不能保证指令执行顺序

下述代码是一个想法很好但是无法实现目的代码:因为尝试使用临时变量强制指定指令运行顺序时,仍然会被编译器认为是无用的变量,然后被优化掉。

Singleton* Singleton::getInstance() {
	if(m_instance== nullptr)
	{
		static mutex mtx;
		lock_guard<mutex> lock(mtx);
		if (m_instance== nullptr)
		{
			auto tmp = new Singleton()
			m_instance= tmp;            
		}
	}
	return instance;
}

使用原子操作的内存顺序

class Singleton
{
  private:
    // static volatile Singleton * volatile local_instance;
    static atomic<Singleton*> instance;
    Singleton(){
        cout << "构造" << endl;
    };
    ~Singleton(){
        cout << "析构" << endl;
    }
    

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

atomic<Singleton*> Singleton::instance;

在C++11中提供一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag。std::call_once是一种lazy load的很简单易用的机制。

#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使用模板

template <class T>
class Singleton
{
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
 
public:
    template <typename... Args>
    static T* instance(Args&&... args)
    {
        std::call_once(_flag, [&](){
            _instance = new T(std::forward<Args>(args)...);
        });
        return _instance;
    }
 
    static void destroy()
    {
        if (_instance)
        {
            delete _instance;
            _instance = NULL;
        }
    }
 
private:
    static T* _instance;
    static std::once_flag _flag;
};
 
template <class T>
T* Singleton<T>::_instance = NULL; 
 
template <class T>
std::once_flag Singleton<T>::_flag;

个人还是推荐使用静态局部变量,代码简单,现在基本所有c++项目都支持c++11。此外,原子操作也值得推荐,因为这确实是从根本上解决了单例的线程安全以及效率问题。
单例的应用

#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;

class TaskQueue
{
public:
    // = delete 代表函数禁用, 也可以将其访问权限设置为私有
    TaskQueue(const TaskQueue& obj) = delete;
    TaskQueue& operator=(const TaskQueue& obj) = delete;
    static TaskQueue* getInstance()
    {
        return &m_obj;
    }
    // 任务队列是否为空
    bool isEmpty()
    {
        lock_guard<mutex> locker(m_mutex);
        bool flag = m_taskQ.empty();
        return flag;
    }
    // 添加任务
    void addTask(int data)
    {
        lock_guard<mutex> locker(m_mutex);
        m_taskQ.push(data);
    }
    // 取出一个任务
    int takeTask()
    {
        lock_guard<mutex> locker(m_mutex);
        if (!m_taskQ.empty())
        {
            return m_taskQ.front();
        }
        return -1;
    }
    // 删除一个任务
    bool popTask()
    {
        lock_guard<mutex> locker(m_mutex);
        if (!m_taskQ.empty())
        {
            m_taskQ.pop();
            return true;
        }
        return false;
    }
private:
    TaskQueue() = default;
    static TaskQueue m_obj;
    queue<int> m_taskQ;
    mutex m_mutex;
};
TaskQueue TaskQueue::m_obj;

int main()
{
    thread t1([]() {
        TaskQueue* taskQ = TaskQueue::getInstance();
        for (int i = 0; i < 100; ++i)
        {
            taskQ->addTask(i + 100);
            cout << "+++push task: " << i + 100 << ", threadID: " 
                << this_thread::get_id() << endl;
            this_thread::sleep_for(chrono::milliseconds(500));
        }
    });
    thread t2([]() {
        TaskQueue* taskQ = TaskQueue::getInstance();
        this_thread::sleep_for(chrono::milliseconds(100));
        while (!taskQ->isEmpty())
        {
            int data = taskQ->takeTask();
            cout << "---take task: " << data << ", threadID: " 
                << this_thread::get_id() << endl;
            taskQ->popTask();
            this_thread::sleep_for(chrono::seconds(1));
        }
    });
    t1.join();
    t2.join();
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值