目录
一、线程安全的单例模式
1、什么是单例模式?
2、单例模式的特点
3、饿汉式与懒汉式实现方式
4、饿汉式实现单例模式
5、懒汉式实现单例模式(非线程安全)
6、懒汉式实现单例模式(线程安全版本)
7、简单归纳懒汉模式的应用场景
1. 懒汉模式核心概念
2. 主要应用场景
3. 使用懒汉模式的考量因素
总结
二、懒汉模式的单例线程池
1、GetInstance() 方法详解
1. 双重检查锁定模式 (Double-Checked Locking Pattern)
2. static Mutex instance_mutex
3. 线程安全分析
2、DestroyInstance() 方法详解
1. 资源清理流程
2. 为什么也需要双重检查?
3. static Mutex 的注意事项
3、改进建议
4、总结
5、补充扩展:static 变量作用域
1. 函数内的 static 变量作用域
2. 实际效果相当于:
3. 演示代码证明
4. 这会带来什么问题?
5. 正确的做法:类静态成员
6. 总结
三、为什么线程池中需要有互斥锁和条件变量?
1、互斥锁:保护任务队列的线程安全
2、条件变量:实现线程的等待与唤醒
3、避免惊群效应
4、临界区与非临界区的分离
四、为什么线程池中的线程执行例程需要设置为静态方法?
1、成员函数的隐藏参数问题
2、静态成员函数的特性
3、通过this指针访问对象成员
4、设计模式建议
总结
一、线程安全的单例模式
1、什么是单例模式?
单例模式(Singleton Pattern)是一种创建型设计模式,确保某个类在全局范围内仅有一个实例,并提供全局访问点。其核心思想是通过控制类的实例化过程,避免重复创建对象,从而节省资源、保证数据一致性。
类比理解:
现实中的单例:一个男人只能有一个妻子(法律约束)。
技术场景:服务器启动时加载上百GB的共享数据(如配置、缓存),需通过单例类集中管理,避免内存浪费和状态不一致。
2、单例模式的特点
唯一性:类有且仅有一个实例。
全局访问:通过静态方法(如
GetInstance())提供全局访问入口。(核心要点:static 的函数或变量不需要创建类的对象就可以直接使用!!!)延迟初始化(可选):根据需求决定是否在首次使用时创建实例(懒汉式)。
资源优化:避免重复创建高开销对象(如数据库连接池、线程池)。
典型应用场景:
配置管理类(加载全局配置文件)。
日志记录器(统一输出日志)。
对象池、连接池等资源管理类。
3、饿汉式与懒汉式实现方式
类比说明:
饿汉式:类似“吃完饭立刻洗碗”,提前初始化实例,确保随时可用。
懒汉式:类似“吃完饭先放碗,下次用时再洗”,延迟初始化以优化启动速度。
核心区别:
| 特性 | 饿汉式 | 懒汉式 |
|---|---|---|
| 初始化时机 | 程序启动时 | 首次调用GetInstance()时 |
| 性能影响 | 启动稍慢,运行无锁 | 启动快,首次调用有锁 |
| 适用场景 | 实例必用且初始化开销小 | 实例可能不用或初始化开销大 |
4、饿汉式实现单例模式
template
class Singleton {
private:static T data; // 类加载时初始化
public:static T* GetInstance() {return &data;}
};
// 需在类外定义静态成员(分配内存)
template
T Singleton::data;
特点:
线程安全:类加载阶段完成初始化,无多线程问题。
潜在问题:若实例未被使用,会造成资源浪费。
5、懒汉式实现单例模式(非线程安全)
template
class Singleton {
private:static T* inst; // 延迟初始化
public:static T* GetInstance() {if (inst == nullptr) { // 首次检查inst = new T(); // 非原子操作,线程不安全}return inst;}
};
// 需在类外初始化指针
template
T* Singleton::inst = nullptr;
问题:
竞态条件:若两个线程同时通过首次检查,会创建多个实例。
适用场景:仅限单线程环境。
6、懒汉式实现单例模式(线程安全版本)
#include
template
class Singleton {
private:volatile static T* inst; // volatile防止编译器优化static std::mutex lock; // 互斥锁保护初始化
public:static T* GetInstance() {if (inst == nullptr) { // 第一次检查(减少锁竞争)std::lock_guard guard(lock); // 加锁if (inst == nullptr) { // 第二次检查(确保唯一性)inst = new T();}}return inst;}
};
// 初始化静态成员
template
volatile T* Singleton::inst = nullptr;
template
std::mutex Singleton::lock;
关键优化点:
双重检查锁定(DCL):
首次
if减少锁竞争(多数情况下直接返回已初始化实例)。第二次
if确保仅一个线程执行初始化。
volatile关键字:防止编译器对指针赋值操作进行优化(如重排序),确保多线程下可见性。lock_guard简化锁管理:RAII机制自动释放锁,避免手动解锁遗漏。
注意事项:
C++11前的DCL问题:旧标准中
volatile无法完全保证内存顺序,需用内存屏障。C++11后new T()是原子操作,配合volatile可安全使用。更优方案:C++11推荐使用局部静态变量(天然线程安全):
templateclass Singleton { public:static T& GetInstance() {static T inst; // 线程安全初始化return inst;} };
7、简单归纳懒汉模式的应用场景
1. 懒汉模式核心概念
特点:只在第一次需要时才创建实例,延迟初始化
2. 主要应用场景
资源密集型对象管理:数据库连接池、线程池、大型缓存系统、文件管理器
配置信息管理:全局配置管理器、应用设置中心、环境变量管理器
硬件资源控制:打印机假脱机、设备驱动程序、串口/USB端口管理
服务定位器模式:微服务架构中的服务发现、API网关实例、消息队列连接
状态管理:游戏引擎中的场景管理器、用户会话管理器、应用程序状态机
工具类实例:日志记录器、性能监控器、异常处理器
3. 使用懒汉模式的考量因素
✅ 适合使用的情况:
对象创建成本高(内存、时间)
不一定会被用到
需要全局唯一访问点
初始化依赖运行时信息
❌ 不适合使用的情况:
对象必须程序启动就存在
创建成本很低
需要频繁创建销毁
多线程环境复杂且性能敏感
懒汉模式本质上是一种"按需分配"的设计思想,在资源受限或初始化成本高的场景下特别有用。
总结
饿汉式:简单可靠,适合实例必用且初始化快的场景。
懒汉式(线程安全):通过DCL和锁机制平衡延迟初始化与线程安全,但需注意C++标准兼容性。
现代C++推荐:优先使用局部静态变量实现单例,代码简洁且线程安全。
单例模式的核心是控制实例化时机与保证全局唯一性,根据实际场景选择合适实现方式。
二、懒汉模式的单例线程池
修改 ThreadPool.hpp 文件,将其改为懒汉模式的单例线程池:
#pragma once
#include
#include
#include
#include
#include
#include "Log.hpp" // 引⼊⾃⼰的⽇志
#include "Thread.hpp" // 引⼊⾃⼰的线程
#include "Mutex.hpp" // 引⼊⾃⼰的锁
#include "Cond.hpp" // 引⼊⾃⼰的条件变量
using namespace ThreadModule;
using namespace CondModule;
using namespace MutexModule;
using namespace LogModule;
const static int gdefaultthreadnum = 10;
static std::string GetThreadNameFromNptl()
{char thread_name[16] = {0};pthread_getname_np(pthread_self(), thread_name, sizeof(thread_name));return std::string(thread_name);
}
// ⽇志
template
class ThreadPool
{
private:void HandlerTask() // 类的成员⽅法,也可以成为另⼀个类的回调⽅法,⽅便我们继续类级别的互相调⽤!{std::string name = GetThreadNameFromNptl();LOG(LogLevel::INFO) << name << " is running...";while (true){// 1. 保证队列安全_mutex.Lock();// 2. 队列中不⼀定有数据while (_task_queue.empty() && _isrunning){_waitnum++;_cond.Wait(_mutex);_waitnum--;}// 2.1 如果线程池已经退出了 && 任务队列是空的if (_task_queue.empty() && !_isrunning){_mutex.Unlock();break;}// 2.2 如果线程池不退出 && 任务队列不是空的// 2.3 如果线程池已经退出 && 任务队列不是空的 --- 处理完所有的任务,然后在退出// 3. ⼀定有任务, 处理任务T t = _task_queue.front();_task_queue.pop();_mutex.Unlock();LOG(LogLevel::DEBUG) << name << " get a task";// 4. 处理任务,这个任务属于线程独占的任务t();}}// 私有构造函数,防止外部实例化ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum), _waitnum(0), _isrunning(false){LOG(LogLevel::INFO) << "ThreadPool Construct()";}// 禁止拷贝构造和赋值操作ThreadPool(const ThreadPool&) = delete;ThreadPool& operator=(const ThreadPool&) = delete;
public:// 获取单例实例的静态方法static ThreadPool* GetInstance(int threadnum = gdefaultthreadnum){static Mutex instance_mutex; // 用于保护单例创建的互斥锁if (_instance == nullptr){LockGuard lockguard(instance_mutex);if (_instance == nullptr) // 双重检查锁定{_instance = new ThreadPool(threadnum);}}return _instance;}// 销毁单例实例static void DestroyInstance(){static Mutex instance_mutex;if (_instance != nullptr){LockGuard lockguard(instance_mutex);if (_instance != nullptr){_instance->Stop();_instance->Wait();delete _instance;_instance = nullptr;}}}void InitThreadPool(){// 指向构建出所有的线程,并不启动for (int num = 0; num < _threadnum; num++){_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this));LOG(LogLevel::INFO) << "init thread " << _threads.back().Name() << " done";}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();LOG(LogLevel::INFO) << "start thread " << thread.Name() << "done";}}void Stop(){_mutex.Lock();_isrunning = false;_cond.NotifyAll();_mutex.Unlock();LOG(LogLevel::DEBUG) << "线程池退出中...";}void Wait(){for (auto &thread : _threads){thread.Join();LOG(LogLevel::INFO) << thread.Name() << " 退出...";}}bool Enqueue(const T &t){bool ret = false;_mutex.Lock();if (_isrunning){_task_queue.push(t);if (_waitnum > 0){_cond.Notify();}LOG(LogLevel::DEBUG) << "任务⼊队列成功";ret = true;}_mutex.Unlock();return ret;}~ThreadPool(){LOG(LogLevel::INFO) << "ThreadPool Destruct()";}
private:static ThreadPool* _instance; // 静态单例指针int _threadnum;std::vector _threads; // for fix, int tempstd::queue _task_queue;Mutex _mutex;Cond _cond;int _waitnum;bool _isrunning;
};
// 静态成员变量定义
template
ThreadPool* ThreadPool::_instance = nullptr;
同时,需要修改 Main.cc 文件来适应单例模式的使用:
#include
#include
#include "ThreadPool.hpp"
using namespace std;
// 简单的任务函数
void taskFunction(int id) {cout << "Task " << id << " is being processed by thread " << GetThreadNameFromNptl() << endl;sleep(1); // 模拟任务处理时间cout << "Task " << id << " completed" << endl;
}
int main() {// 启用控制台日志策略Enable_Console_Log_Strategy();// 获取线程池单例,3个线程ThreadPool>* pool = ThreadPool>::GetInstance(3);// 初始化并启动线程池pool->InitThreadPool();pool->Start();// 添加10个任务到线程池for (int i = 0; i < 10; ++i) {// 使用lambda捕获i的值pool->Enqueue([i]() {taskFunction(i);});sleep(1); // 每隔1秒添加一个任务}// 等待所有任务完成sleep(2);// 停止并销毁线程池单例cout << "Stopping thread pool..." << endl;ThreadPool>::DestroyInstance();cout << "All tasks completed, thread pool stopped." << endl;return 0;
}
主要修改内容:
私有构造函数:将构造函数设为私有,防止外部直接实例化
删除拷贝构造和赋值操作:防止通过拷贝方式创建多个实例
静态实例指针:添加静态的
_instance指针来保存单例GetInstance方法:提供获取单例的静态方法,使用双重检查锁定确保线程安全
DestroyInstance方法:提供销毁单例的静态方法
修改Main.cc:更新使用方式,通过
GetInstance()获取单例,通过DestroyInstance()销毁单例
这样修改后,线程池就变成了懒汉模式的单例,确保在整个程序中只有一个线程池实例存在。
1、GetInstance() 方法详解
static ThreadPool* GetInstance(int threadnum = gdefaultthreadnum)
{static Mutex instance_mutex; // 用于保护单例创建的互斥锁if (_instance == nullptr){LockGuard lockguard(instance_mutex);if (_instance == nullptr) // 双重检查锁定{_instance = new ThreadPool(threadnum);}}return _instance;
}
1. 双重检查锁定模式 (Double-Checked Locking Pattern)
这是实现线程安全懒汉单例的经典模式:
// 第一次检查(不加锁)
if (_instance == nullptr)
{// 加锁进入临界区LockGuard lockguard(instance_mutex);// 第二次检查(加锁后)if (_instance == nullptr){_instance = new ThreadPool(threadnum);}
}
为什么要双重检查?
第一次检查:避免每次调用都加锁,提高性能
第二次检查:防止多个线程同时通过第一次检查后重复创建实例
2. static Mutex instance_mutex
static Mutex instance_mutex;
static确保所有线程共享同一个互斥锁只在第一次调用
GetInstance()时初始化用于保护单例的创建过程
3. 线程安全分析
假设两个线程同时调用 GetInstance():
线程A 线程B
↓ ↓
第一次检查: null 第一次检查: null
↓ ↓
获取锁 等待锁
↓ ↓
第二次检查: null (等待中)
↓
创建实例
↓
释放锁↓获取锁↓第二次检查: 非null ← 实例已创建↓释放锁
2、DestroyInstance() 方法详解
static void DestroyInstance()
{static Mutex instance_mutex;if (_instance != nullptr){LockGuard lockguard(instance_mutex);if (_instance != nullptr){_instance->Stop();_instance->Wait();delete _instance;_instance = nullptr;}}
}
1. 资源清理流程
_instance->Stop(); // 1. 停止接收新任务
_instance->Wait(); // 2. 等待所有任务完成
delete _instance; // 3. 释放内存
_instance = nullptr; // 4. 重置指针
2. 为什么也需要双重检查?
if (_instance != nullptr) // 第一次检查
{LockGuard lockguard(instance_mutex);if (_instance != nullptr) // 第二次检查{// 销毁操作...}
}
防止的问题:
线程A通过第一次检查后,线程B可能已经销毁了实例
加锁后再次检查确保实例仍然存在
3. static Mutex 的注意事项
在两个方法中都使用了:
static Mutex instance_mutex;
由于是函数内的 static 变量:
在
GetInstance()和DestroyInstance()中是不同的互斥锁这实际上是一个设计缺陷!
3、改进建议
当前代码的问题:两个方法使用不同的互斥锁,可能无法完全保证线程安全。
改进方案:
class ThreadPool
{
private:static ThreadPool* _instance;static Mutex _instance_mutex; // 改为类静态成员
public:static ThreadPool* GetInstance(int threadnum = gdefaultthreadnum){if (_instance == nullptr){LockGuard lockguard(_instance_mutex); // 使用同一个锁if (_instance == nullptr){_instance = new ThreadPool(threadnum);}}return _instance;}static void DestroyInstance(){LockGuard lockguard(_instance_mutex); // 使用同一个锁if (_instance != nullptr){_instance->Stop();_instance->Wait();delete _instance;_instance = nullptr;}}
};
// 在类外定义
template
ThreadPool* ThreadPool::_instance = nullptr;
template
Mutex ThreadPool::_instance_mutex;
4、总结
这个实现采用了:
懒汉模式:只在第一次使用时创建实例
双重检查锁定:平衡性能与线程安全
RAII锁管理:使用
LockGuard自动管理锁的生命周期完整的生命周期管理:提供创建和销毁的对称接口
虽然当前的实现存在互斥锁分离的小问题,但整体架构是正确的单例模式实现。
5、补充扩展:static 变量作用域
1. 函数内的 static 变量作用域
static ThreadPool* GetInstance(int threadnum = gdefaultthreadnum)
{static Mutex instance_mutex; // 这个 static 是函数作用域的// ...
}
static void DestroyInstance()
{static Mutex instance_mutex; // 这个 static 也是函数作用域的// ...
}
关键点:虽然都是 static,但它们属于不同的函数,所以是不同的变量!
2. 实际效果相当于:
// 编译器实际上会生成类似这样的代码:
Mutex& GetInstance_instance_mutex() {static Mutex mutex; // 每个函数有自己的静态变量return mutex;
}
Mutex& DestroyInstance_instance_mutex() {static Mutex mutex; // 这是另一个不同的静态变量return mutex;
}
static ThreadPool* GetInstance(int threadnum = gdefaultthreadnum)
{if (_instance == nullptr){LockGuard lockguard(GetInstance_instance_mutex()); // 锁A// ...}
}
static void DestroyInstance()
{static Mutex instance_mutex;if (_instance != nullptr){LockGuard lockguard(DestroyInstance_instance_mutex()); // 锁B// ...}
}
3. 演示代码证明
#include
#include
class Test {
public:static void func1() {static std::mutex mtx; // func1 的静态mutexstd::cout << "func1 mutex address: " << &mtx << std::endl;}static void func2() {static std::mutex mtx; // func2 的静态mutexstd::cout << "func2 mutex address: " << &mtx << std::endl;}
};
int main() {Test::func1(); // 输出地址 ATest::func2(); // 输出地址 B ≠ Areturn 0;
}
输出结果:

4. 这会带来什么问题?
竞态条件场景:
// 线程A执行
ThreadPool* pool = ThreadPool::GetInstance(); // 使用锁A
// 同时线程B执行
ThreadPool::DestroyInstance(); // 使用锁B
// 可能发生:
// 1. 线程A通过第一次检查 _instance == nullptr
// 2. 线程B销毁实例并设置 _instance = nullptr
// 3. 线程A获取锁A,再次检查 _instance == nullptr (因为线程B用了不同的锁)
// 4. 线程A重新创建实例!导致内存泄漏和逻辑错误
5. 正确的做法:类静态成员
template
class ThreadPool
{
private:static ThreadPool* _instance;static Mutex _instance_mutex; // 类静态成员,所有方法共享
public:static ThreadPool* GetInstance(int threadnum = gdefaultthreadnum){if (_instance == nullptr){LockGuard lockguard(_instance_mutex); // 所有方法用同一个锁if (_instance == nullptr){_instance = new ThreadPool(threadnum);}}return _instance;}static void DestroyInstance(){LockGuard lockguard(_instance_mutex); // 所有方法用同一个锁if (_instance != nullptr){_instance->Stop();_instance->Wait();delete _instance;_instance = nullptr;}}
};
// 类外定义
template
ThreadPool* ThreadPool::_instance = nullptr;
template
Mutex ThreadPool::_instance_mutex; // 真正的全局唯一
6. 总结
函数内的 static:每个函数有自己的静态变量实例
类内的 static:所有方法共享同一个静态变量
单例模式中:必须使用类静态成员作为互斥锁,确保所有方法同步访问
这就是为什么原代码中的设计存在线程安全问题!
三、为什么线程池中需要有互斥锁和条件变量?
在线程池的实现中,互斥锁和条件变量是保障线程安全与高效协作的核心机制,其必要性源于任务队列作为共享临界资源的特性以及线程间复杂的同步需求。以下是详细分析:
1、互斥锁:保护任务队列的线程安全
临界资源问题:任务队列是多个线程(包括生产者线程和消费者线程)并发访问的共享数据结构。若不加保护,多线程同时修改队列(如入队、出队)会导致数据竞争,引发队列状态不一致、任务丢失或重复执行等问题。
互斥锁的作用:通过加锁机制,确保同一时刻只有一个线程能操作任务队列。例如,当线程A从队列中取出任务时,其他线程必须等待锁释放后才能访问队列,从而避免并发修改的冲突。
2、条件变量:实现线程的等待与唤醒
空队列等待:消费者线程从队列中取任务前,需检查队列是否为空。若为空,线程应阻塞(而非忙等待),以节省CPU资源。此时需要条件变量配合互斥锁实现等待机制。
任务到达唤醒:当生产者线程向队列中添加任务后,需通知等待的消费者线程。条件变量的
signal或broadcast操作能高效唤醒阻塞线程,使其重新检查队列状态。伪唤醒与重复检查:线程被唤醒时可能因伪唤醒(无明确原因的唤醒)或广播唤醒(多个线程被唤醒但仅一个能获取任务)导致条件不满足。因此,必须用
while循环重复检查队列是否非空,而非if语句,以避免逻辑错误。
3、避免惊群效应
问题描述:使用
pthread_cond_broadcast会唤醒所有等待线程,但若队列中只有一个任务,大量线程同时竞争会导致系统资源浪费(如上下文切换开销),甚至引发性能震荡(惊群效应)。解决方案:优先使用
pthread_cond_signal唤醒单个线程,减少无效竞争。仅在需要唤醒多个线程时(如批量任务)使用broadcast。
4、临界区与非临界区的分离
任务处理的位置:线程从队列中取出任务后,应在释放锁后再执行任务。原因如下:
避免长时间占用锁:任务处理可能耗时较长(如I/O操作),若在加锁期间执行,会阻塞其他线程访问队列,降低并发效率。
并行性保障:锁仅保护队列操作,任务处理本身应并行执行,以充分发挥多线程优势。若处理在临界区内,线程池将退化为串行执行。
四、为什么线程池中的线程执行例程需要设置为静态方法?
在C++中使用pthread_create创建线程时,执行例程(Routine)的签名必须为void* (*)(void*),而类的成员函数隐含this指针作为第一个参数,导致直接使用非静态成员函数作为例程会引发编译错误。以下是具体原因与解决方案:
1、成员函数的隐藏参数问题
非静态成员函数:包含一个隐式的
this指针参数,指向调用该函数的对象实例。例如:class ThreadPool { public:void workerThread(); // 实际签名:void workerThread(ThreadPool* this) };若直接将其作为
pthread_create的例程,参数数量不匹配(例程需1个参数,成员函数实际需2个),导致编译失败。
2、静态成员函数的特性
无
this指针:静态成员函数属于类而非对象实例,不依赖this指针,因此其签名与pthread_create要求的例程完全一致:class ThreadPool { public:static void* workerThread(void* arg); // 符合要求 };
3、通过this指针访问对象成员
传递对象实例:在创建线程时,将当前对象的
this指针作为参数传递给静态例程:pthread_create(&tid, nullptr, &ThreadPool::workerThread, this);调用非静态方法:在静态例程内部,通过
this指针转换为对象类型后,调用非静态成员函数(如Pop任务):void* ThreadPool::workerThread(void* arg) {ThreadPool* pool = static_cast(arg);pool->Pop(); // 调用非静态方法// ... }
4、设计模式建议
封装线程逻辑:将线程例程设计为静态方法,通过参数传递对象上下文,既满足线程库接口要求,又能灵活访问对象状态。
避免全局状态:若不使用
this指针,需通过全局变量或单例模式共享状态,但会引入耦合性和线程安全问题,不如对象封装优雅。
总结
互斥锁与条件变量:互斥锁保护任务队列的并发访问,条件变量实现线程的等待-通知机制,二者协作确保线程安全与高效协作。
静态例程的必要性:静态方法消除了
this指针的隐式参数问题,通过显式传递对象指针实现与非静态成员的交互,是线程与对象结合的常见模式。
通过合理使用这些机制,线程池能够高效管理任务分配与线程执行,充分发挥多线程的并发优势。