C++并发编程(6):单例模式、once_flag与call_once、call_once实现单例

单例模式

参考博客

【C++】单例模式(饿汉模式、懒汉模式)
C++单例模式总结与剖析
饿汉单例模式 C++实现
C++单例模式(饿汉式)

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结

,一共有23种经典设计模式

使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性

设计模式使代码编写真正工程化,设计模式是软件工程的基石脉络,如同大厦的结构一样

单例模式是设计模式中最常用的一种模式,一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享

基础要点:

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

单例的实现主要有饿汉式和懒汉式两种,分别进行介绍

饿汉式

不管你将来用不用,程序启动时就创建一个唯一的实例对象

优点:简单

缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定

示例代码:

cpp 复制代码
//  Hunger_Singleton_pattern
//  Created by lei on 2022/05/13

#include <iostream>
#include <memory>
using namespace std;

class Example
{
public:
    typedef shared_ptr<Example> Ptr;
    static Ptr GetSingleton()
    {
        cout << "Get Singleton" << endl;
        return single;
    }

    void test()
    {
        cout << "Instance location:" << this << endl;
    }

    ~Example() { cout << "Deconstructor called" << endl; };

private:
    static Ptr single;
    Example() { cout << "Constructor called" << endl; };
    Example &operator=(const Example &examp) = delete;
    Example(const Example &examp) = delete;
};
Example::Ptr Example::single = shared_ptr<Example>(new Example);

int main()
{
    Example::Ptr a = Example::GetSingleton();
    Example::Ptr b = Example::GetSingleton();
    a->test();
    b->test();
    cout << "main end" << endl;
    return 0;
}

打印输出:

cpp 复制代码
Constructor called
Get Singleton
Get Singleton
Instance location:0x55d43c9dce70
Instance location:0x55d43c9dce70
main end
Deconstructor called

可以看到拷贝构造函数只调用了一次,并且两个对象内存地址相同,说明该类只能实例化一个对象

饿汉单例模式的静态变量的初始化由C++完成,规避了线程安全问题,所以饿汉单例模式是线程安全的

在大多数情况下使用饿汉单例模式是没有问题的

有缺陷的懒汉模式

懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存

cpp 复制代码
//  Defect_Lazy_Singleton_pattern
//  Created by lei on 2022/05/13

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

class Singleton
{
private:
    Singleton()
    {
        cout << "constructor called!" << endl;
    }
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton &) = delete;
    static Singleton *m_instance_ptr;

public:
    ~Singleton()
    {
        cout << "destructor called!" << endl;
    }
    static Singleton *get_instance()
    {
        if (m_instance_ptr == nullptr)
        {
            m_instance_ptr = new Singleton;
        }
        return m_instance_ptr;
    }
    void use() const { cout << "in use" << endl; }
};

Singleton *Singleton::m_instance_ptr = nullptr;     //静态成员变量类内声明类外初始化

int main()
{
    Singleton *instance = Singleton::get_instance();
    Singleton *instance_2 = Singleton::get_instance();

    // thread t1(Singleton::get_instance);
    // thread t2(Singleton::get_instance);
    // thread t3(Singleton::get_instance);
    // thread t4(Singleton::get_instance);

    // t1.join();
    // t2.join();
    // t3.join();
    // t4.join();

    return 0;
}

打印输出:

cpp 复制代码
constructor called!

取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,存在以下问题

1、当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr

是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr

还是空的,于是也开始实例化单例;这样就会实例化出两个对象

2、类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用,因此会导致内存泄漏

改进的懒汉模式

对应上面两个问题,有以下解决方法:

1、用mutex加锁

2、使用智能指针

cpp 复制代码
//  Improve_Lazy_Singleton_pattern
//  Created by lei on 2022/05/13

#include <iostream>
#include <memory> // shared_ptr
#include <mutex>  // mutex
#include <thread>
using namespace std;

class Singleton
{
public:
    typedef shared_ptr<Singleton> Ptr;
    ~Singleton()
    {
        cout << "destructor called!" << endl;
    }
    Singleton(const Singleton &) = delete;
    Singleton& operator=(const Singleton &) = delete;
    static Ptr get_instance()
    {
        // "double checked lock"
        if (m_instance_ptr == nullptr)
        {
            lock_guard<mutex> lk(m_mutex);
            if (m_instance_ptr == nullptr)
            {
                m_instance_ptr = shared_ptr<Singleton>(new Singleton);
            }
        }
        return m_instance_ptr;
    }

private:
    Singleton()
    {
        cout << "constructor called!" << endl;
    }
    static Ptr m_instance_ptr;
    static mutex m_mutex;
};

// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
mutex Singleton::m_mutex;

int main()
{
    Singleton::Ptr instance = Singleton::get_instance();
    Singleton::Ptr instance2 = Singleton::get_instance();

    // thread t1(Singleton::get_instance);
    // thread t2(Singleton::get_instance);
    // thread t3(Singleton::get_instance);
    // thread t4(Singleton::get_instance);

    // t1.join();
    // t2.join();
    // t3.join();
    // t4.join();

    return 0;
}

打印输出:

cpp 复制代码
constructor called!
destructor called!

只构造了一次实例,并且发生了析构

缺陷是双检锁依然会失效,具体原因可以看下面的文章

https://www.drdobbs.com/cpp/c-and-the-perils-of-double-checked-locki/184405726

推荐的懒汉模式

cpp 复制代码
//  Recommand_Lazy_Singleton_pattern
//  Created by lei on 2022/05/13

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

class Singleton
{
public:
    ~Singleton()
    {
        cout << "destructor called!" << endl;
    }
    Singleton(const Singleton &) = delete;
    Singleton &operator=(const Singleton &) = delete;
    static Singleton &get_instance()
    {
        static Singleton instance;
        return instance;
    }

private:
    Singleton()
    {
        cout << "constructor called!" << endl;
    }
};

int main()
{
    Singleton &instance_1 = Singleton::get_instance();
    Singleton &instance_2 = Singleton::get_instance();

    // thread t1(Singleton::get_instance);
    // thread t2(Singleton::get_instance);
    // thread t3(Singleton::get_instance);
    // thread t4(Singleton::get_instance);

    // t1.join();
    // t2.join();
    // t3.join();
    // t4.join();

    return 0;
}

打印输出:

cpp 复制代码
constructor called!
destructor called!

这种方法又叫做 Meyers' Singleton Meyer'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

如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束

这是最推荐的一种单例实现方式:

  • 通过局部静态变量的特性保证了线程安全
  • 不需要使用共享指针,代码简洁
  • 注意在使用的时候需要声明单例的引用 Single& 才能获取对象

once_flag与call_once

参考博客

C++11于once flag,call_once:分析的实现
C++11实现线程安全的单例模式(使用std::call_once)

在多线程编程中,有一个常见的情景是某个任务仅仅须要运行一次

在C++11中提供了非常方便的辅助类once_flag与call_once

once_flag和call_once的声明:

cpp 复制代码
struct once_flag
{
    constexpr once_flag() noexcept;
    once_flag(const once_flag&) = delete;
    once_flag& operator=(const once_flag&) = delete;
};
template<class Callable, class ...Args>
  void call_once(once_flag& flag, Callable&& func, Args&&... args);

}  // std

简单示例:

cpp 复制代码
//  once_flag and call_once simple example
//  Created by lei on 2022/05/13

#include <iostream>
using namespace std;

once_flag flag;

void do_once()
{
    call_once(flag, [&]()
              { cout << "Called once" << endl; });
}

int main()
{
    std::thread t1(do_once);
    std::thread t2(do_once);
    std::thread t3(do_once);
    std::thread t4(do_once);

    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

打印输出:

cpp 复制代码
Called once

可以看到4个线程只执行了一次do_once( )函数

call_once实现单例模式

cpp 复制代码
//  Call_once_Singleton_pattern
//  Created by lei on 2022/05/13

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

once_flag cons_flag;

class A
{
public:
    typedef shared_ptr<A> Ptr;

    void m_print() { cout << "m_a++ = " << ++m_a << endl; }

    static Ptr getInstance(int a)
    {
        cout << "Get instance" << endl;
        if (m_instance_ptr == nullptr)
        {
            lock_guard<mutex> m_lock(m_mutex);
            if (m_instance_ptr == nullptr)
            {
                call_once(cons_flag, [&]()
                          { m_instance_ptr.reset(new A(a)); });
            }
        }
        return m_instance_ptr;
    }

    ~A()
    {
        cout << "Deconstructor called" << endl;
    }

private:
    static mutex m_mutex;
    int m_a;
    static Ptr m_instance_ptr;

    A(int a_) : m_a(a_)
    {
        cout << "Constructor called" << endl
             << "m_a = " << m_a << endl;
    }

    A &operator=(const A &A_) = delete;

    A(const A &A_) = delete;
};
A::Ptr A::m_instance_ptr = nullptr;
mutex A::m_mutex;

void test(int aa)
{
    cout << "Go in test..." << endl;
    A::Ptr tp = A::getInstance(aa);
    cout << "tp location:" << tp << endl;
    tp->m_print();
    cout << endl;
}

int main()
{
    thread t1(test, 1);
    thread t2(test, 2);
    thread t3(test, 3);
    thread t4(test, 4);

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    cout << "main end..." << endl;

    return 0;
}

打印输出:

cpp 复制代码
Go in test...
Get instance
Constructor called
m_a = 4
tp location:0x7fd964000f30
m_a++ = 5

Go in test...
Get instance
tp location:0x7fd964000f30
m_a++ = 6

Go in test...
Get instance
tp location:0x7fd964000f30
m_a++ = 7

Go in test...
Get instance
tp location:0x7fd964000f30
m_a++ = 8

main end...
Deconstructor called

看到构造函数只调用了一次,并且类A实例化对象的地址始终相同

上面的两个示例程序中都用到了lambda表达式,call_once通常结合lambda一起使用

相关推荐
qq_433554543 分钟前
C++ 面向对象编程:递增重载
开发语言·c++·算法
易码智能12 分钟前
【EtherCATBasics】- KRTS C++示例精讲(2)
开发语言·c++·kithara·windows 实时套件·krts
ཌ斌赋ད18 分钟前
FFTW基本概念与安装使用
c++
若川44 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
薄荷故人_1 小时前
从零开始的C++之旅——红黑树封装map_set
c++
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
悲伤小伞1 小时前
C++_数据结构_详解二叉搜索树
c语言·数据结构·c++·笔记·算法
m0_675988232 小时前
Leetcode3218. 切蛋糕的最小总开销 I
c++·算法·leetcode·职场和发展
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
code04号5 小时前
C++练习:图论的两种遍历方式
开发语言·c++·图论