章节重点
面试中经常被问到如何实现线程安全的单例模式,网上的答案大多都是懒汉式,饿汉式前篇一律的回答,老廖今天分享一个线程安全的单例模板类,让每个类基于这个单例模板类都能实现单例模式,在大家在面试中能够脱颖而出。
重点内容
视频讲解:《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值)
典型应用场景
单例模式的实现
全局资源的初始化
线程安全的延迟初始化
共享资源的首次访问初始化
这个函数在实现线程安全的单例模式时特别有用,因为它能确保在多线程环境下初始化代码只执行一次。
函数: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. 掌握线程安全的单例模板类如何实现
主要是面试该如何讲解单例的线程安全,由以下几个点:
- 基于pthread_once构建一个线程安全的单例模板类,确保创建对应的单例对象只被执行一次;
- pthread_once的控制变量 也是静态变量,static pthread_once_t ponce_; 保证确保线程安全初始化的pthread_once_t变量
- 实现单例模板类的好处在于 项目中要实现单例模式的类只需要使用这个单例模板类实例化即可,这样更好的做到代码复用。比如测试范例的 Test Test2类。