《C++Linux编程进阶:从0实现muduo 》-第7讲.C++线程安全的单例模式

发布于:2025-04-01 ⋅ 阅读:(98) ⋅ 点赞:(0)

章节重点

面试中经常被问到如何实现线程安全的单例模式,网上的答案大多都是懒汉式,饿汉式前篇一律的回答,老廖今天分享一个线程安全的单例模板类,让每个类基于这个单例模板类都能实现单例模式,在大家在面试中能够脱颖而出。

重点内容

视频讲解:《C++Linux编程进阶:从0实现muduo C++网络框架系列》-第7讲.C++线程安全的单例模式

代码改动

lesson7代码

  • 实现:base/Singleton.h

  • examples/test_once.c test_singleton.cc

1 pthread_once函数使用

在多线程编程中,有些操作只需要执行一次。pthread_once函数可以确保某个初始化操作只执行一次,无论有多少线程尝试执行该操作。

1.1 函数原型

我来详细解释 pthread_once 函数的原型和说明:

函数原型

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

参数说明

once_control:指向 pthread_once_t 类型的指针

  • 这是一个控制变量,用于确保初始化函数只执行一次

  • 必须初始化为 PTHREAD_ONCE_INIT

  • 类型定义:typedef int pthread_once_t;

init_routine:初始化函数指针

  • 类型为 void (*)(void)

  • 这是一个无参数、无返回值的函数

  • 这个函数只会被执行一次

返回值

  • 成功:返回 0

  • 失败:返回错误码(非0值)

典型应用场景

  1. 单例模式的实现

  2. 全局资源的初始化

  3. 线程安全的延迟初始化

  4. 共享资源的首次访问初始化

这个函数在实现线程安全的单例模式时特别有用,因为它能确保在多线程环境下初始化代码只执行一次。

函数:int atexit(void (*func)(void));

  • func:要注册的退出处理函数指针     类型为 void (*)(void)

  • 这是一个无参数、无返回值的函数

  • 这个函数会在程序正常退出时被调用

在单例模板类中用于程序退出后释放单例对象的资源。

1.2 测试范例

测试范例 examples/test_once.cc

#include <pthread.h>
#include <iostream>

// 全局变量
pthread_once_t once_control = PTHREAD_ONCE_INIT;
pthread_once_t once_control2 = PTHREAD_ONCE_INIT;
// 初始化函数
void init()
{
    std::cout << "init这个函数在多线程中只执行一次" << std::endl;
}

void init2()
{
    std::cout << "init2这个函数在多线程中只执行一次" << std::endl;
}
// 退出函数
void cleanup()
{
    std::cout << "cleanup function called" << std::endl;
}

// 退出函数
void cleanup2()
{
    std::cout << "cleanup2 function called" << std::endl;
}

void init_once()
{
    pthread_once(&once_control, init);
    pthread_once(&once_control, init2);
    std::cout << "call to pthread_once:" << std::endl;  //这里是可以多次调用
}

int main()
{
   init_once();  //调用两次,只打印一次
   init_once();

    // 注册退出函数
    atexit(cleanup);
    atexit(cleanup2);

    std::cout << "\nProgram will exit, cleanup function should be called" << std::endl;
    return 0;
}

执行输出

lqf@ubuntu:~/spark_muduo/lesson7/build$ ./bin/test_once

init这个函数在多线程中只执行一次

call to pthread_once:

call to pthread_once:

Program will exit, cleanup function should be called

cleanup2 function called

cleanup function called

2 Singleton线程安全的单例模板类

这个单例模板完整的技术原理还是比较复杂,但我们重点掌握pthread_once调用的意义即可。

2.1 多线程访问时序图

2.2 关键时序说明

1.构造过程:

  • 首次调用 instance() 触发 pthread_once

  • pthread_once 确保 init() 只执行一次

  • 创建对象并检查是否需要自动销毁

  • 返回对象引用

2.析构过程:

  • 程序退出时触发 atexit 注册的函数

  • 执行 destroy() 进行类型检查

  • 安全删除对象并置空指针

3.多线程安全:

  • pthread_once 保证初始化只执行一次

  • 其他线程等待初始化完成

  • 所有线程获得相同的对象引用

4.no_destroy 机制:

  • 编译期检查是否定义了 no_destroy

  • 决定是否注册 atexit 回调

  • 影响对象的自动销毁行为

  • 这个机制比较复杂,目前不建议深究,比如要单例处理的类 使用静态变量`static const bool no_destroy = true; 则封装的单例模板类Singleton不会自动析构Test对象。

class Test
{
public:
    
    // 如果不需要自动销毁,可以定义这个静态成员,不定义时默认自动销毁
    static const bool no_destroy = true;

private:
    std::string name_;
};

2.3 关键点说明

1.线程安全保证:

  • 使用 pthread_once 确保线程安全

  • 静态成员变量的初始化是线程安全的

template<typename T>
class Singleton : noncopyable
{
    // 用于确保线程安全初始化的pthread_once_t变量
    static pthread_once_t ponce_;
    // 单例对象的指针
    static T* value_;
}
  • 对象创建过程是原子的

static void init()
{
    value_ = new T();
}
//使用pthread_once确保 Singleton::init只被执行一次。
pthread_once(&ponce_, &Singleton::init);  

2.4 测试范例

examples/test_singleton.cc

#include "base/Singleton.h"


#include <stdio.h>
#include <unistd.h>
#include <string>
#include <pthread.h>
#include <memory>
class Test
{
public:
    Test()
    {
        printf("构造Test %p\n", this);
    }

    ~Test()
    {
        printf("析构 Test %p %s\n", this, name_.c_str());
    }
    // 如果不需要自动销毁,可以定义这个静态成员
    // static const bool no_destroy = true;
    const std::string& name() const { return name_; }
    void setName(const std::string& n) { name_ = n; }

private:
    std::string name_;
};

class Test2
{
public:
    Test2()
    {
        printf("构造Test2 %p\n", this);
    }

    ~Test2()
    {
        printf("析构 Test2 %p %s\n", this, name_.c_str());
    }
    // 如果不需要自动销毁,可以定义这个静态成员
    // static const bool no_destroy = true;
    const std::string& name() const { return name_; }
    void setName(const std::string& n) { name_ = n; }

private:
    std::string name_;
};

// 测试普通单例
void testSingleton()
{
    printf("testSingleton\n");
    // 测试Test
    printf("%p name=%s\n",
           &mymuduo::Singleton<Test>::instance(),
           mymuduo::Singleton<Test>::instance().name().c_str());
    mymuduo::Singleton<Test>::instance().setName("only one");

    // 测试Test2
    printf("%p name=%s\n",
           &mymuduo::Singleton<Test2>::instance(),
           mymuduo::Singleton<Test2>::instance().name().c_str());
    mymuduo::Singleton<Test2>::instance().setName("only one2");
}



int main()
{
    printf("main thread start\n");

    // 测试普通单例
    testSingleton();
    printf("%p name=%s\n",
           &mymuduo::Singleton<Test>::instance(),
           mymuduo::Singleton<Test>::instance().name().c_str());
 

    printf("main thread exit\n");
    return 0;
} 

运行输出

main thread start

构造Test 0x563df212c6c0

0x563df212c6c0 name=

构造Test2 0x563df212c6f0

0x563df212c6f0 name=

0x563df212c6c0 name=only one

main thread exit

析构 Test2 0x563df212c6f0 only one2

析构 Test 0x563df212c6c0 only one

3 章节重点

重点:

1. 确保掌握pthread_once是如何做到其调用的函数只初始化一次

2. 静态变量的初始化是线程安全的

3. 掌握线程安全的单例模板类如何实现

主要是面试该如何讲解单例的线程安全,由以下几个点:

  1. 基于pthread_once构建一个线程安全的单例模板类,确保创建对应的单例对象只被执行一次;
  2. pthread_once的控制变量 也是静态变量,static pthread_once_t ponce_; 保证确保线程安全初始化的pthread_once_t变量
  3. 实现单例模板类的好处在于 项目中要实现单例模式的类只需要使用这个单例模板类实例化即可,这样更好的做到代码复用。比如测试范例的 Test Test2类。

网站公告

今日签到

点亮在社区的每一天
去签到