设计模式之单例模式(C++)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 设计模式之单例模式(C++)

介绍

单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;


常见的运用场景

当对象需要被共享的时候又或者某类需要频繁实例化.

例如:

  • 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
  • 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
  • 回收站,在整个系统运行过程中,回收站一直维护着仅有的一个实例;
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。

实现要点

  • 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
  • 线程安全
  • 禁止赋值和拷贝
  • 用户通过接口获取实例:使用 static 类成员函数

单例模式的写法(C++)

单例模式的写法分为两种,区别在于实例化的时机.

  • 饿汉式(Eager initialization)

像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例,这是一种空间换时间的做法.作为一个饿汉而言,体现了它的本质——“我全都要”.


  • 优缺点

优点:

  1. 避免了线程同步问题.
  2. 线程安全在类加载的同时已经创建好一个静态对象,调用时反应速度快.

缺点:

  1. 来类装载的时候就完成了实例化,没有达到Lazy Loading的效果.
  2. 如果从始至终未使用过这个实例,则会造成实例的浪费.

  • 普遍方式

代码示例:

#pragma once
#include <iostream>
class Singleton {
 //构造器私有化
private:
   Singleton() {}
   Singleton(const Singleton&)
   Singleton& operator=(const Singleton&);
//想要这个实例在程序运行的整个过程中都存在,所以我们不允许实例自己主动调用析构函数释放对象.
    ~Singleton(){) 
    
   static Singleton instance;     //方式一:类对象
     static Singleton* instance =new Singleton(); //方式二:类对象的指针
//提供一个公有的静态方法,返回实例对象.
public :
   static Singleton getInstance()  { return &instance; } //方式一:返回类对象
   static Singleton* Singleton::getInstance(){ return instance; }//方式二:返回类对象指针
   static void deleteInstance()  { delete instance; } //方式二:用来销毁对象的指针的实例
 };

  • shared_ptr与自定义删除器方式

代码示例:

#pragma once
#include <iostream>
#include <memory>
using namespace std;
class Singleton{
private:
    Singleton(){
        cout <<__TIME__<<":" << __FUNCTION__<< endl;
    }
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
    ~Singleton(){
        cout <<__TIME__<<":" <<__FUNCTION__ << endl;
    }
    static void DestroyInstance(Singleton*);    //自定义一个释放实例的函数
    static shared_ptr<Singleton> instance;  //这是我们的单例对象,它是一个类对象的指> >     
public:
    static shared_ptr<Singleton> getInstance();
    shared_ptr<Singleton> Singleton::instance(new Singleton(),Singleton::DestroyInstance); 
    shared_ptr<Singleton> Singleton::getInstance(){ return instance;  }
    void Singleton::DestroyInstance(Singleton*){ cout <<__TIME__<<":" << __FUNCTION__<< endl;}
  };
  • 懒汉式(Lazy-Initialization)

像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法.
懒汉式的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。


  • 优缺点

优点:

单例对象的生成是在用户需要使用的时候再去构造,提高了应用的启动速度.

缺点:

需要为保证线程安全增加额外的开销.


  • 线程安全、内存安全的懒汉式单例 (智能指针,锁)

优点:

1.基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉,以此避免内存泄漏.
2.加了锁,使用互斥量来达到线程安全。使用两个 if判断语句的技术称为双检锁,好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的.
缺点:
1.使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束.
2.使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好.
3.在某些平台(与编译器和指令集架构有关),双检锁会失效.


代码示例:

#pragma once
#include <iostream>
#include <memory> // shared_ptr
#include <mutex>  // mutex
class Singleton{
public:
  typedef std::shared_ptr<Singleton> Ptr;
  Singleton(Singleton&)=delete;
  Singleton& operator=(const Singleton&)=delete;
   ~Singleton(){  std::cout<<"destructor called!"<<std::endl; }
  static Ptr get_instance()
   {
        // "double checked lock"
        if(m_instance_ptr==nullptr)
        {
            std::lock_guard<std::mutex> lk(m_mutex);
            if(m_instance_ptr == nullptr)
            {
              m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
            }
        }
        return m_instance_ptr;
    }
private:
    Singleton() {std::cout<<"constructor called!"<<std::endl;}
    static Ptr m_instance_ptr;
    static std::mutex m_mutex;
};
     
// initialization static variables out of class
    Singleton::Ptr Singleton::m_instance_ptr = nullptr;
    std::mutex Singleton::m_mutex;

  • Magic Static式单例(C++11局部静态变量)

这种方法又叫做 Meyers’ SingletonMeyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:

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

如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式.


优点:

1.通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
2.不需要使用共享指针,代码简洁;
3.注意在使用的时候需要声明单例的引用 Single& 才能获取对象。


代码示例:

#pragma once
#include <iostream>
using namespace std;
class Singleton{
public:
  ~Singleton(){cout <<__TIME__<<":"<< __FUNCTION__<< endl;}
  Singleton(const Singleton&)=delete;
  Singleton& operator=(const Singleton&)=delete;
  static Singleton& get_instance()
  {
    static Singleton instance;
    return instance;
  }
private:
  Singleton(){cout <<__TIME__<<":" <<__FUNCTION__<< endl;}
};

为什么采用返回局部静态变量而不返回类内的变量:

C++只能保证在同一个文件中声明的static变量的初始化顺序与其变量声明的顺序一致。但是不能保证不同的文件中的static变量的初始化顺序。
然后对于单例模式而言,不同的单例对象之间进行调用也是常见的场景。比如我有一个单例,存储了程序启动时加载的配置文件的内容。另外有一个单例,掌管着一个全局唯一的日志管理器。在日志管理初始化的时候,要通过配置文件的单例对象来获取到某个配置项,实现日志打印。
这时候两个单例在不同文件中各自实现,很有可能在日志管理器的单例使用配置文件单例的时候,配置文件的单例对象是没有被初始化的。这个未初始化可能产生的风险指的是C++变量的未初始化,而不是说配置文件未加载的之类业务逻辑上的未初始化导致的问题。

单例类模版(C++)

  • CRTP 奇异递归模板模式实现

基类模板的实现要点

1.构造函数需要是 protected,这样子类才能继承.
2.使用了奇异递归模板模式CRTP(Curiously recurring template pattern).
3.get instance 方法和 static local方法一个原理.
4.在这里基类的析构函数可以不需要 virtual ,因为子类在应用中只会用 Derived 类型,保证了析构时和构造时的类型一致.


  • 代码示例
#pragma once
// brief: a singleton base class offering an easy way to create singleton
#include <iostream>
template<typename T> class Singleton{ 
public:
    static T& get_instance()
    {
        static T instance;
        return instance;
    }
    virtual ~Singleton()
    {
        std::cout<<"destructor called!"<<std::endl;
    }
    Singleton(const Singleton&)=delete;
    Singleton& operator =(const Singleton&)=delete;
protected:
    Singleton()
    {
        std::cout<<"constructor called!"<<std::endl;
    }
};
 /********************************************************************************************/
 // Example:
 //1.friend class declaration is requiered!
 // 2.constructor should be private
class DerivedSingle:public Singleton<DerivedSingle>
{   
 // !!!!attention!!!    
 // needs to be friend in order to   
 // access the private constructor/destructor    
    friend class Singleton<DerivedSingle>; 
public:    
    DerivedSingle(const DerivedSingle&)=delete;    
    DerivedSingle& operator =(const DerivedSingle&)= delete; 
private:    
    DerivedSingle()=default;
 };
/********************************************************************************************/
int main(int argc, char* argv[])
{
    DerivedSingle& instance1 = DerivedSingle::get_instance();
    DerivedSingle& instance2 = DerivedSingle::get_instance();
    return 0; 
}

  • 不需要在子类声明友元的实现方法
  • 实现要点

精髓在于使用一个代理类 token,子类构造函数需要传递token类才能构造,把 token保护其起来, 然后子类的构造函数就可以是公有的了,这个子类只有Derived(token)的这样的构造函数,这样用户就无法自己定义一个类的实例了,起到控制其唯一性的作用。


  • 代码示例
#pragma once
 // brief: a singleton base class offering an easy way to create singleton
#include <iostream>
template<typename T> class Singleton{ 
public:
    static T& get_instance() noexcept(std::is_nothrow_constructible<T>::value)
    {
        static T instance{token()};
        return instance;
    }
    virtual ~Singleton() =default;
    Singleton(const Singleton&)=delete;
    Singleton& operator =(const Singleton&)=delete; 
protected:
    struct token{}; // helper class
    Singleton() noexcept=default; 
};
/****************************************************************************************/ 
// Example:
// constructor should be public because protected `token` control theccess
class DerivedSingle:public Singleton<DerivedSingle>{ 
public:   
     DerivedSingle(token){ std::cout<<"destructor called!"<<std::endl;    }
      ~DerivedSingle(){std::cout<<"constructor called!"<<std::endl;    }    
       DerivedSingle(const DerivedSingle&)=delete;    
       DerivedSingle& operator=(const DerivedSingle&)= delete; 
};
/****************************************************************************************/ 
int main(int argc, char* argv[])
{
    DerivedSingle& instance1 = DerivedSingle::get_instance();
    DerivedSingle& instance2 = DerivedSingle::get_instance();
    return 0; 
} 

总结

当你需要系统中只有唯一一个实例存在的类的全局变量的时候才使用单例.
使用时,越小越好,越简单越好,保证线程安全,内存不泄露.

单例模式实现一个进程管理器

头文件:process_monitor.h

#include <iostream>
#include <memory>
#include <unordered_map>
#include <vector>
#include <string>
#include <mutex>
#include <sys/epoll.h>
#include <sys/wait.h>
#include <unistd.h>
class ProcessMonitor {
public:
    // 获取单例实例
    static ProcessMonitor& getInstance();
    ProcessMonitor(const ProcessMonitor&) = delete;
    ProcessMonitor& operator=(const ProcessMonitor&) = delete;
    // 注册要监控的进程
    void registerProcess(const std::string& process_name, const std::string& command);
    // 监控进程状态,并根据需要重启进程
    void monitor();
private:
    // 构造函数和析构函数设置为私有
    ProcessMonitor() = default;
    ~ProcessMonitor() = default;
    // 进程信息结构体
    struct ProcessInfo {
        std::string process_name;
        std::string command;
        int epoll_fd;
        pid_t pid;
    };
    // 启动进程
    void startProcess(std::shared_ptr<ProcessInfo> process_info);
    // 重启进程
    void restartProcess(std::shared_ptr<ProcessInfo> process_info);
    // 存储进程信息的容器
    std::unordered_map<std::string, std::shared_ptr<ProcessInfo>> processes;
    std::mutex processes_mutex;
};

源文件:process_monitor.cpp

#include "process_monitor.h"
// 获取单例实例
ProcessMonitor& ProcessMonitor::getInstance() {
    static ProcessMonitor instance;
    return instance;
}
// 注册要监控的进程
void ProcessMonitor::registerProcess(const std::string& process_name, const std::string& command) {
    std::lock_guard<std::mutex> lock(processes_mutex);
    auto process_info = std::make_shared<ProcessInfo>();
    process_info->process_name = process_name;
    process_info->command = command;
    process_info->epoll_fd = epoll_create1(0);
    process_info->pid = 0;
    processes[process_name] = process_info;
}
// 启动进程
void ProcessMonitor::startProcess(std::shared_ptr<ProcessInfo> process_info) {
    pid_t pid = fork();
    if (pid == 0) {
        // Child process
        system(process_info->command.c_str());
        exit(0);
    } else if (pid > 0) {
        // Parent process
        process_info->pid = pid;
    } else {
        // Error
        std::cerr << "Failed to fork process for " << process_info->process_name << std::endl;
    }
}
// 监控进程状态,并根据需要重启进程
void ProcessMonitor::monitor() {
    // 无限循环以持续监控进程状态
    while (true) {
        // 等待1秒,以便每秒检查一次进程状态
        sleep(1);
        // 对进程容器上锁,以避免在检查状态时其他线程对其进行修改
        std::lock_guard<std::mutex> lock(processes_mutex);
        // 遍历已注册的进程信息
        for (auto& kv : processes) {
            auto& process_info = kv.second;
            // 存储waitpid函数返回的状态信息
            int status;
            // 使用WNOHANG选项非阻塞地检查进程状态
            pid_t result = waitpid(process_info->pid, &status, WNOHANG);
            if (result == 0) {
                // 进程仍在运行
                // 在此处可以添加进程负载量的打印逻辑
            } else if (result == process_info->pid) {
                // 进程已终止,需要重启
                restartProcess(process_info);
            } else {
                // 检查进程状态时发生错误
                std::cerr << "Error checking process status for " << process_info->process_name << std::endl;
            }
        }
    }
}


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
32 2
|
17天前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
|
22天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
23 2
|
1月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
38 4
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
28天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
22 1
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
27 0
|
2月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将通过探讨单例模式,一种最常用的设计模式,来揭示其在PHP中的应用及优势。单例模式确保一个类仅有一个实例,并提供一个全局访问点。通过实际案例,我们将展示如何在PHP项目中有效实现单例模式,以及如何利用这一模式优化资源配置和管理。无论是PHP初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧,进而提升自己的编程实践。
|
2月前
|
设计模式 安全 Java
C# 一分钟浅谈:设计模式之单例模式
【10月更文挑战第9天】单例模式是软件开发中最常用的设计模式之一,旨在确保一个类只有一个实例,并提供一个全局访问点。本文介绍了单例模式的基本概念、实现方式(包括饿汉式、懒汉式和使用 `Lazy&lt;T&gt;` 的方法)、常见问题(如多线程和序列化问题)及其解决方案,并通过代码示例详细说明了这些内容。希望本文能帮助你在实际开发中更好地应用单例模式,提高代码质量和可维护性。
70 1