单例设计模式共享数据分析、解决、call_once

本文探讨了单例设计模式在多线程环境下的应用,重点分析了如何确保线程安全地创建单例对象,介绍了互斥量、双重检查锁定及std::call_once()函数的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

互斥量是最通用的保护共享数据的机制,用起来不难,而且特别好用。
当然也有其他保护共享数据的机制。

一、设计模式大概谈

“设计模式”:代码的一些写法(这些写法根常规写法不怎么一样):程序灵活,维护起来很方便,但是别人接管、阅读代码都会很痛苦;
用“设计模式”理念写出来的代码是很晦涩的;
老外应付特别大的项目的时候,把项目的开发经验、模块划分经验,总结成设计模式(现有开发需求,后又理论总结和整理)
设计模式拿到中国来,不太一样,拿着一个程序(项目)往设计模式上套,一个小小的项目,非要弄几个设计模式进去,本末倒置。
设计模式肯定有它独特的优点,要活学活用,不要生搬硬套。

二、单例设计模式

单例设计模式,使用的频率比较高;
单例:整个项目中,有某个或者某些特殊的类,属于该类的对象,我只能创建一个,多了我创建不了。

所谓单例:是整个项目中,有某个或者某些特殊的类,只能创建一个该类的对象。

单例类

class MyCAS
{
private:
    MyCAS() {}

private:
    static MyCAS* m_instance;

public:
    static MyCAS* GetInstance()
    {
        if (m_instance == NULL)
        {
            m_instance = new MyCAS();
            static CGarhuishou cl;
        }
        return m_instance;
    }

    class CGarhuishou   //类中套类,用来释放对象
    {
    public:
        ~CGarhuishou()
        {
            if (MyCAS::m_instance)
            {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };

    void func()
    {
        cout << "测试" << endl;
    }
};

//类静态变量初始化
MyCAS* MyCAS::m_instance = NULL;

int main()
{
    MyCAS* p_a = MyCAS::GetInstance();  //创建一个对象,返回该类(MyCAS)对象的指针
    MyCAS* p_b = MyCAS::GetInstance();
    //p_a、p_b都指向了同一个对象,所以说MyCAS是一个单例类
    p_a->func();
}

三、单例设计模式共享数据问题分析、解决

面临的问题:需要在我们自己创建的线程(而不是主线程)中来创建MyCAS这个单例类的对象,这种线程可能不止一个(最少2个)
我们可能面临GetInstance()这种成员函数要互斥;

void mythread()
{
    cout << "我的线程开始执行了" << endl;
    MyCAS* p_a = MyCAS::GetInstance();
    cout << "我的线程执行完毕了" << endl;
    return;
}

int main()
{
    std::thread myobj1(mythread);
    std::thread myobj2(mythread);
    myobj1.join();
    myobj2.join();
}

虽然这两个线程是同一个入口函数,但大家千万要记住,这是两个线程,所以这里会有两个流程(两条通路)同时开始执行mythread这个函数

std::mutex resource_mutex;
    static MyCAS* GetInstance()
    {
        std::unique_lock<std::mutex> mymutex(resource_mutex);
        if (m_instance == NULL)
        {
            m_instance = new MyCAS();
            static CGarhuishou cl;
        }
        return m_instance;
    }

上面的写法不够高效,更高效的方式:

static MyCAS* GetInstance()
{
    //a) 如果if (m_instance != NULL)条件成立,则表示m_instance已经被new过了;
    //b) 如果if (m_instance == 	NULL),不代表m_instance一定没被new过;
    //比如说,在没加锁的情况下,线程1刚要执行m_instance = new MyCAS();结果突然切到线程2,线程2new了一把之后切回到线程1,线程1继续new
    if (m_instance == NULL)	//双重锁定(双重检查)
    {
        std::unique_lock<std::mutex> mymutex(resource_mutex);
        if (m_instance == NULL)
        {
            m_instance = new MyCAS();
            static CGarhuishou cl;
        }
    }
    return m_instance;
}

小结
只有在未初始化时,去判断
if (m_instance == NULL),去加锁
初始化之后的后续都不需要加锁

本节中共享数据m_instance = new MyCAS();,多个线程同时访问执行的,
通过std::unique_lock临界上,最外面加上双重锁定来提高多次调用GetInstance的效率

四、std::call_once()

C++11引入的函数,该函数的第二个参数是一个函数名。
call_once的功能是能够保证函数a()只被调用一次。
比如,两个线程都调用函数a,只要用call_once,就能保证a函数只被调用1次。
call_once具备互斥量这种能力,而且效率上比互斥量的资源消耗更少。
call_once()需要与一个标记结合使用,这个标记std::once_flag。其实once_flag是一个结构,call_once就是通过这个标记决定对应的函数a()是否执行,调用call_once()成功后,call_once()就把这个标记设置为一种“已调用”的状态,后续再次调用call_once,只要once_flag被设置为了“已调用”状态,那么函数a()就不会再执行了

例子:

std::once_flag g_flag;
class MyCAS
{
    static void CreateInstance()
    {
        m_instance = new MyCAS();
        static CGarhuishou cl;
    }
private:
    MyCAS() {}

private:
    static MyCAS* m_instance;

public:
    static MyCAS* GetInstance()
    {
        std::call_once(g_flag,CreateInstance);//两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕CreateInstance()
        return m_instance;
    }

    class CGarhuishou   //类中套类,用来释放对象
    {
    public:
        ~CGarhuishou()
        {
            if (MyCAS::m_instance)
            {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };

    void func()
    {
        cout << "测试" << endl;
    }
};

可以把std::once_flag g_flag看成一把锁
(感觉还是没听懂)
举个例子:
参考:C++11 std::call_once:保证函数在任何情况下只调用一次

#include <iostream>
#include <thread>
#include <mutex>
 
std::once_flag flag1;
 
void simple_do_once()
{
    std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}
 
int main()
{
    std::thread st1(simple_do_once);
    std::thread st2(simple_do_once);
    std::thread st3(simple_do_once);
    std::thread st4(simple_do_once);
    st1.join();
    st2.join();
    st3.join();
    st4.join();
}

结果:

Simple example: called once
### 使用 `std::call_once` 创建模式 为了确保线程安全并仅初始化一次对象,可以利用 C++11 中引入的 `std::call_once` 函数配合 `std::once_flag` 来实现模式。这种方式能够有效防止多线程环境下多次实化问题的发生。 下面是一个基于上述方法构建的 Singleton 类模板示: ```cpp #include <iostream> #include <mutex> template<typename T> class Singleton { private: static T* instance; static std::once_flag initFlag; protected: // 构造函数设为保护类型以阻止外部直接创建对象 Singleton() {} public: ~Singleton() {} // 获取唯一实的方法 static T& GetInstance() { std::call_once(initFlag, [](){ instance = new T(); }); return *instance; } // 删除拷贝构造函数和赋值操作符来保证一性 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; // 静态成员变量定义与初始化 template<typename T> T* Singleton<T>::instance = nullptr; template<typename T> std::once_flag Singleton<T>::initFlag; int main() { auto& obj = Singleton<MyClass>::GetInstance(); // 进一步的操作... } ``` 此代码片段展示了如何通过静态局部变量以及 lambda 表达式的组合方式,在首次调用 `GetInstance()` 方法时完成类实的一次性初始化过程[^1]。 同时也删除了复制控制成员(即拷贝构造函数和赋值运算符),从而确保该类会被意外地重复实化[^2]。 值得注意的是,这种方法依赖于 C++11 或更新的标准支持;因此对于某些旧版编译器可能无法正常工作[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值