C++11中的std::call_once 使用举例 + 实现Singleton模式程序示例

         某些场景下,我们需要代码只被执行一次,比如单例类的初始化,考虑到多线程安全,需要进行加锁控制。C++11中提供的call_once可以很好的满足这种需求,使用又非常简单。

#include<mutex>
template <class Fn, class... Args>
void call_once (once_flag& flag, Fn&& fn, Args&&...args);

        第一个参数是std::once_flag的对象(once_flag是不允许修改的,其拷贝构造函数和operator=函数都声明为delete),第二个参数可调用实体,即要求只执行一次的代码,后面可变参数是其参数列表。

       call_once保证函数fn只被执行一次,如果有多个线程同时执行函数fn调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态)——不会直接返回,直到活动线程对fn调用结束才返回。对于所有调用函数fn的并发线程,数据可见性都是同步的(一致的)。
       如果活动线程在执行fn时抛出异常,则会从处于”passive execution”状态的线程中挑一个线程成为活动线程继续执行fn,依此类推。一旦活动线程返回,所有”passive execution”状态的线程也返回,不会成为活动线程。(实际上once_flag相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程)。

程序示例:
static std::once_flag oc;  // 用于call_once的局部静态变量
Singleton* Singleton::m_instance = nullptr;

Singleton* Singleton::getInstance() 
{
     std::call_once(oc, [&] () { m_instance = newSingleton(); });
     return m_instance;
}

注意: once_flag的生命周期,它必须要比使用它的线程的生命周期要长。所以通常定义成全局变量比较好。 

【扩展】实现Singleton模式(设计一个类,我们只能生成该类的一个实例)

<1-单线程解法> 缺点:多线程情况下,每个线程可能创建出不同的的Singleton实例
#include <iostream>
using namespace std;

class Singleton
{
public:
    static Singleton* getInstance()
    {
        if(m_pInstance == nullptr)
        {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }
    static void destroyInstance()
    {
        if(m_pInstance != nullptr)
        {
            delete m_pInstance;
            m_pInstance = nullptr;
        }    
  }
private:
    Singleton(){}
    static Singleton* m_pInstance;
};

Singleton* Singleton::m_pInstance = nullptr;//静态成员变量要在类外初始化
// 单线程获取多次实例
void Test1(){
    // 预期结果:两个实例指针指向的地址相同
    Singleton* singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
    Singleton* singletonObj2 = Singleton::getInstance();
    cout << singletonObj2 << endl;
    Singleton::destroyInstance();
}

int main()
{
    Test1();
    return 0;
}

         解法一是最简单,也是最普遍的实现方式。但是,这种实现方式有很多问题,比如没有考虑多线程的问题,在多线程的情况下,就可能会创建多个Singleton实例,以下是改善的版本。

<2- 多线程+加锁>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;

class Singleton
{
private:
    static mutex m_mutex; // 互斥量
    Singleton(){}
    static Singleton* m_pInstance;
public:
    static Singleton* getInstance(){
        if(m_pInstance == nullptr){
            m_mutex.lock(); // 使用C++11中的多线程库
            if(m_pInstance == nullptr){ // 两次判断是否为NULL的双重检查
                m_pInstance = new Singleton();
            }
            m_mutex.unlock();
        }
        return m_pInstance;
    }
    static void destroyInstance(){
        if(m_pInstance != nullptr){
            delete m_pInstance;
            m_pInstance = nullptr;
        }
    }
};

Singleton* Singleton::m_pInstance = nullptr;
mutex Singleton::m_mutex;

void print_singleton_instance(){
    Singleton *singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
}
// 多个进程获得单例
void Test1(){
    // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_singleton_instance));
    }
    for(auto& thr : threads){
        thr.join();
    }
}

int main(){
    Test1();
    Singleton::destroyInstance();
    return 0;
}

       此方法中进行了两次m_pInstance == nullptr的判断,使用了所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,只在m_pInstance不为nullptr时才需要加锁,同时也保证了线程安全。但是,如果进行大数据的操作,加锁操作将成为一个性能的瓶颈,为此,一种新的单例模式的实现也就出现了。如下:

<3 - const static型实例>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
class Singleton
{
private:
    Singleton(){}
    static const Singleton* m_pInstance;
public:
    static Singleton* getInstance(){
        return const_cast<Singleton*>(m_pInstance); // 去掉“const”特性
    }
    static void destroyInstance(){
        if(m_pInstance != NULL){
            delete m_pInstance;
            m_pInstance = NULL;
        }
    }
};
// 利用const只能定义一次,不能再次修改的特性,static继续保持类内只有一个实例
const Singleton* Singleton::m_pInstance = new Singleton(); 

void print_singleton_instance(){
    Singleton *singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
}
// 多个进程获得单例
void Test1(){
    // 预期结果,打印出相同的地址,之间可能缺失换行符,也属正常现象
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_singleton_instance));
    }
    for(auto& thr : threads){
        thr.join();
    }
}
int main(){
    Test1();
    Singleton::destroyInstance();
    return 0;
}

       因为静态初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。由于上述三种实现,都要考虑到实例的销毁,关于实例的销毁,待会在分析。

<4 - 在get函数中创建并返回static临时实例的引用>//PS:该方法不能认为控制单例实例的销毁
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
class Singleton
{
private:
    Singleton(){}
public:
    static Singleton* getInstance(){
        static Singleton m_pInstance; // 注意,声明在该函数内
        return &m_pInstance;
    }
};
void print_singleton_instance(){
    Singleton *singletonObj = Singleton::getInstance();
    cout << singletonObj << endl;
}
// 多个进程获得单例
void Test1(){
   // 预期结果,打印出相同的地址
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_singleton_instance));
    }
    for(auto& thr : threads){
        thr.join();
    }
}
// 单个进程获得多次实例
void Test2(){
    // 预期结果,打印出相同的地址
    print_singleton_instance();
    print_singleton_instance();
}
int main(){
    cout << "Test1 begins: " << endl;
    Test1();
    cout << "Test2 begins: " << endl;
    Test2();
    return 0;
}

       在实际项目中,特别是客户端开发,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,他就自然而然结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。但是,有以下情况,是必须要进行实例销毁的:在类中,有一些文件锁了文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放。

在程序

<5- 最终方案,最简&显式控制实例销毁>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
class Singleton
{
private:
    Singleton(){}
    static Singleton* m_pInstance;
    // **重点在这**
    class GC // 类似Java的垃圾回收器
    {
    public:
        ~GC(){
            // 可以在这里释放所有想要释放的资源,比如数据库连接,文件句柄……等等。
            if(m_pInstance != NULL){
                cout << "GC: will delete resource !" << endl;
                delete m_pInstance;
                m_pInstance = NULL;
            }
        };
    };
    // 内部类的实例
    static GC gc;
public:
    static Singleton* getInstance(){
        return m_pInstance;
    }
};

Singleton* Singleton::m_pInstance = new Singleton();
Singleton::GC Singleton::gc; //这里是定义吗,但并没有给出初值-调用默认构造函数
void print_instance(){
    Singleton* obj1 = Singleton::getInstance();
    cout << obj1 << endl;
}
// 多线程获取单例
void Test1(){
    // 预期输出:相同的地址
    vector<thread> threads;
    for(int i = 0; i < 10; ++i){
        threads.push_back(thread(print_instance));
    }
    for(auto& thr : threads){
        thr.join();
    }
}
// 单线程获取单例
void Test2(){
    // 预期输出:相同的地址,换行符分隔
    print_instance();
    print_instance();
    print_instance();
}
int main()
{
    cout << "Test1 begins: " << endl;
    cout << "预期输出:相同的地址" << endl;
    Test1();
    cout << "Test2 begins: " << endl;
    cout << "预期输出:相同的地址" << endl;
    Test2();
    return 0;
}

       运行结束时,系统会调用Singleton的静态成员GC的析构函数,该析构函数会进行资源的释放,而这种资源的释放方式是在程序员“不知道”的情况下进行的,而程序员不用特别的去关心,使用单例模式的代码时,不必关心资源的释放。
       那么这种实现方式的原理是什么呢?由于程序在结束的时候,系统会自动析构所有的全局变量,系统也会析构所有类的静态成员变量,因为静态变量和全局变量在内存中,都是存储在静态存储区的,所有静态存储区的变量都会被释放。由于此处是用了一个内部GC类,而该类的作用就是用来释放资源。这种技巧在C++中是广泛存在的,参见《C++中的RAII机制》。
  

 

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值