设计模式——C++11实现单例模式(饿汉模式、懒汉模式),与单例的进程

8 篇文章 0 订阅

本文将介绍单例模式,使用C++11实现多个版本的单例模式,分析各自的优缺点。最后提及如何实现一个单例的进程。

什么是单例模式

单例模式属于创建型模式,提供了一种创建对象的方式。
单例模式确保一个类只有一个实例。通过一个类统一地访问这个实例。
思想:将构造函数设置为私有,通过一个接口获取类对象。如果对象则创建,否则直接返回。

最简单的单例模式——线程不安全

class Singleton_1
{
public:
    static Singleton_1* GetInstance()
    {
        if(m_instance == nullptr)
        {
            m_instance = new Singleton_1();
        }
        return m_instance;
    }

private:
    Singleton_1() = default;
    Singleton_1(const Singleton_1&) = delete;
    Singleton_1(Singleton_1&&) = delete;	/* 不需要 */
	Singleton_1& operator=(const Singleton_1&) = delete;
	Singleton_1& operator=(Singleton_1&&) = delete;	/* 不需要 */
private:
    static Singleton_1* m_instance;
};
Singleton_1* Singleton_1::m_instance = nullptr;

首先把构造函数全部私有,无参构造选择默认生成,拷贝构造和赋值运算符重载选择删除。不需要显式把移动构造和移动赋值删除,因为右值会默认匹配到const左值引用。
GetInstance:判断类内静态成员instance是否为空,空则new一个对象,否则直接返回。
这样的程序在单执行流下是没问题的,但是如果是多线程,可能导致:

  • 线程A进入了GetInstance,判断instance为空,则准备new对象。在执行new之前由于系统调度,线程被切走,记录了上下文,此时属于就绪态,在就绪队列中排队。此时instance仍为空。
  • 线程B进入了GetInstance,判断instance也是为空,准备new对象。此时instance为空,但是有两个线程进入了作用域,就出现了问题。
  • 最终会new出来两个对象,一个丢失在内存中,导致内存泄露,一个后续会被真正使用。

由于m_instance在多线程情况下是临界资源,所以可以考虑加锁。

线程安全的单例模式——多次锁

class Singleton_2
{
public:
    static Singleton_2* GetInstance()
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        if(m_instance == nullptr)
        {
            m_instance = new Singleton_2();
        }
        return m_instance;
    }

private:
    Singleton_2() = default;
    Singleton_2(const Singleton_2&) = delete;
	Singleton_2& operator=(const Singleton_2&) = delete;

private:
    static Singleton_2* m_instance;
    static std::mutex m_mutex;
};
Singleton_2* Singleton_2::m_instance = nullptr;

在进入GetInstance的时候就加锁,因为要访问instance这个临界资源,才能保证每次只有一个线程在读或写这个资源。同时注意unique_lock构造时传入的mutex也应该是static的。因为static函数中没有this指针,编译器找不到这个mutex,因此需要声明一个静态的mutex。

但是问题在于,每次调用GetInstance的时候,无论对象是否已经实例化,都需要加锁再解锁。这样是有问题的,我们期望在对象没创建之前就要加锁,创建之后已经没必要了,因为每次只去获取对象。
基于上面的缺点,引入了一种解决方式,双重检测。

线程安全的单例模式——单次锁

class Singleton_3
{
public:
    static Singleton_3* GetInstance()
    {
        if(m_instance == nullptr)
        {
            std::unique_lock<std::mutex> lock(m_mutex);
            if(m_instance == nullptr)
                m_instance = new Singleton_3();
        }
        return m_instance;
    }

private:
    Singleton_3() = default;
    Singleton_3(const Singleton_3&) = delete;
	Singleton_3& operator=(const Singleton_3&) = delete;

private:
    static Singleton_3* m_instance;
    static std::mutex m_mutex;
};

对m_instance做两次检查。

  • 第一次:如果已经实例化,直接返回。否则尝试获取锁。
  • 第二次:获取锁后,再次判断是否已经实例化,如果是则返回,否则再实例化对象。
    这样的好处是,就算多个线程判断到了m_instance为空,进入了if,也能保证线程安全地实例化对象。并且实例化之后,后续获取对象都不需要用到锁,相比第二种方式提高了运行的效率。

Meyers单例模式——C++11

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

private:
	Singleton() = default;
	Singleton(const Singleton& other) = delete;
	Singleton& operator=(const Singleton&) = delete;
};

Meyers单例是利用了C++11的特性而实现的单例模式,大道至简。主要依赖于C++11及以后,静态局部变量的初始化是线程安全的,能够保证安全和效率性。

饿汉模式(Eager Initialization)

饿汉模式是指:一个对象在程序正式被执行之前就实例化。
C/C++中,静态变量或者全局变量是存储在静态区。局部静态变量在运行时分配,全局变量、类内静态成员在编译时分配。因此可以定义m_instance为静态成员变量,并且初始化的时候分配内存。

class Singleton_Eager
{
public:
    static Singleton_Eager* GetInstance()
    {
        return m_instance;
    }
private:
    Singleton_Eager() = default;
    Singleton_Eager(const Singleton_Eager& other) = delete;
	Singleton_Eager& operator=(const Singleton_Eager&) = delete;

private:
    static Singleton_Eager* m_instance;
};
Singleton_Eager* Singleton_Eager::m_instance = new Singleton_Eager();

这样的写法使得每次调用GetInstance获取对象都是线程安全的。

懒汉模式(Lazy Initialization)

懒汉模式是指:在第一次要访问对象的时候再实例化。
本篇文章除了上述的饿汉模式,其他都是懒汉模式。

饿汉和懒汉的区别

饿汉模式
优点:运行时速度快,不存在线程安全的问题。
缺点:程序运行前会分配内存,如果对象比较大,或者要求进程启动时间短的场景可能不适用。

懒汉模式
优点:=-=
缺点:需要控制线程互斥,运行时再加载。

如何实现单例的进程?

在某些场景,比如网络服务程序、嵌入式系统的应用如车机仪表和中控,需要控制同一时间只能有一个程序加载到内存中并运行。
Linux提供了文件锁的接口,可以达到这个目的。

/* Operations for the `flock' call.  */
#define	LOCK_SH	1	/* Shared lock.  */
#define	LOCK_EX	2 	/* Exclusive lock.  */
#define	LOCK_UN	8	/* Unlock.  */

/* Can be OR'd in to one of the above.  */
#define	LOCK_NB	4	/* Don't block when locking.  */

/* Apply or remove an advisory lock, according to OPERATION,
   on the file FD refers to.  */
extern int flock (int __fd, int __operation) __THROW;

Apply or remove an advisory lock on the open file specified by fd.  The argument operation is one of the following:
应用或者去除一个建议性质的锁

LOCK_SH  Place a shared lock.  More than one process may hold a shared lock for a given file at a given time.

LOCK_EX  Place an exclusive lock.  Only one process may hold an exclusive lock for a given file at a given time.

LOCK_UN  Remove an existing lock held by this process.

       A call to flock() may block if an incompatible lock is held by another process.  To make a nonblocking request, include LOCK_NB (by ORing) with any of the above
       operations.

       A single file may not simultaneously have both shared and exclusive locks.

       Locks created by flock() are associated with an open file description (see open(2)).  This means that duplicate  file  descriptors  (created  by,  for  example,
       fork(2) or dup(2)) refer to the same lock, and this lock may be modified or released using any of these file descriptors.  Furthermore, the lock is released ei‐
       ther by an explicit LOCK_UN operation on any of these duplicate file descriptors, or when all such file descriptors have been closed.

       If a process uses open(2) (or similar) to obtain more than one file descriptor for the same file, these file descriptors are treated independently  by  flock().
       An attempt to lock the file using one of these file descriptors may be denied by a lock that the calling process has already placed via another file descriptor.

       A  process  may hold only one type of lock (shared or exclusive) on a file.  Subsequent flock() calls on an already locked file will convert an existing lock to
       the new lock mode.

       Locks created by flock() are preserved across an execve(2).

       A shared or exclusive lock can be placed on a file regardless of the mode in which the file was opened.

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
#include <unistd.h>
#include <sys/file.h>

int main()
{
    /* 
        进程单例
        Linux为解决多进程读写同一文件时的冲突,引入了flock
        但是这个锁不是物理上的锁,而是建议性质的
        其他进程可以忽略该锁而直接进行读写
    */
   std::string str = "Hello";

   printf("main : %s\n", str);
    umask(0);
    signal(2, handler);
    std::string fileName = ".lock";
    int fd = open(fileName.c_str(), O_CREAT | O_RDONLY, 0666);
    if(fd < 0)
    {
        printf("fd = %d\n", fd);
        exit(-1);
    }
	
	/* 在打开一个文件后,尝试对其加锁,如果加锁失败,则直接退出 */
    int ret = flock(fd, LOCK_EX | LOCK_NB);
    if(ret < 0)
    {
        printf("ret = %d, what = %s\n",fd, strerror(errno));
        exit(-1);
    }
    while(true)
    {

    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值