
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一. 池化技术与线程池:为什么我们需要线程池?](#一. 池化技术与线程池:为什么我们需要线程池?)
-
- [1.1 池化技术的核心思想](#1.1 池化技术的核心思想)
- [1.2 线程池的核心定义](#1.2 线程池的核心定义)
- [1.3 线程池的核心优势](#1.3 线程池的核心优势)
- [1.4 典型应用场景](#1.4 典型应用场景)
- [二. 线程池的核心设计原理:本质是生产者消费者模型](#二. 线程池的核心设计原理:本质是生产者消费者模型)
-
- [2.1 线程池的核心组成](#2.1 线程池的核心组成)
- [2.2 线程池的核心运行流程](#2.2 线程池的核心运行流程)
- [2.3 核心设计要点](#2.3 核心设计要点)
- [三. 手撕线程池:C++ 源码深度解析](#三. 手撕线程池:C++ 源码深度解析)
-
- [3.1 基础组件:RAII 风格的互斥锁与条件变量封装](#3.1 基础组件:RAII 风格的互斥锁与条件变量封装)
-
- [3.1.1 互斥锁与锁守卫封装](#3.1.1 互斥锁与锁守卫封装)
- [3.1.2 条件变量封装](#3.1.2 条件变量封装)
- [3.2 线程池核心类实现](#3.2 线程池核心类实现)
-
- [3.2.1 线程类定义](#3.2.1 线程类定义)
- [3.2.2 任务类型定义](#3.2.2 任务类型定义)
- [3.2.3 线程池核心类框架(大致接口有那些)](#3.2.3 线程池核心类框架(大致接口有那些))
- [3.2.4 核心成员函数实现](#3.2.4 核心成员函数实现)
- [3.3 线程池使用示例(ThreadPool_v1完整版)](#3.3 线程池使用示例(ThreadPool_v1完整版))
- [四. 进阶优化:线程安全的单例模式线程池(ThreadPool_v2)](#四. 进阶优化:线程安全的单例模式线程池(ThreadPool_v2))
-
- [4.1 单例模式的核心要求](#4.1 单例模式的核心要求)
- [4.2 饿汉模式 vs 懒汉模式](#4.2 饿汉模式 vs 懒汉模式)
- [4.3 线程安全的懒汉单例线程池(双检锁 DCL)](#4.3 线程安全的懒汉单例线程池(双检锁 DCL))
- [4.4 单例线程池使用示例](#4.4 单例线程池使用示例)
- [五. 线程池背后的核心安全问题](#五. 线程池背后的核心安全问题)
-
- [5.1 线程安全与函数可重入](#5.1 线程安全与函数可重入)
- [5.2 死锁:多线程编程的头号杀手](#5.2 死锁:多线程编程的头号杀手)
- [5.3 STL 容器与智能指针的线程安全](#5.3 STL 容器与智能指针的线程安全)
- [5.4 常见锁概念拓展](#5.4 常见锁概念拓展)
- 结尾:
前言:
在 Linux 后端高并发开发场景中,我们经常会遇到这样的问题:WEB 服务器每秒要处理上千次客户端请求,日志系统需要异步写入海量数据,批量计算任务需要并行执行。如果每次处理任务都临时创建线程,不仅会带来巨大的线程创建 / 销毁系统开销,还可能因峰值期创建大量线程导致 CPU 调度过载、甚至系统 OOM。池化技术正是为了解决这类问题而生,而线程池就是池化思想在多线程编程中最经典的工程落地。它通过提前创建一批固定数量的工作线程,统一管理任务队列,让用户任务被复用的线程异步执行,从根本上解决了频繁创建线程的开销问题,同时实现了对并发数的精准控制。本文将从线程池的核心原理出发,带你手撕工业级 C++ 线程池的完整实现,再到单例模式的进阶优化,最后深入拆解线程池背后的线程安全、死锁、锁机制等核心知识点,帮你彻底掌握 Linux 高并发编程的这一核心技能。
一. 池化技术与线程池:为什么我们需要线程池?
1.1 池化技术的核心思想
池化技术的本质是 「提前申请、重复利用、统一管理」,和我们生活中的「预制菜」「共享单车」逻辑完全一致:
- 提前申请资源,避免临时申请的开销;
- 资源重复利用,最大化资源利用率;
- 统一管理资源,避免无节制申请导致系统过载。
除了线程池,我们熟知的进程池、内存池、连接池、对象池,都是池化思想的落地实现。
1.2 线程池的核心定义
线程池是一种线程使用模式:程序启动时提前创建一批固定数量的工作线程,这些线程循环从任务队列中获取用户投递的任务并执行;用户无需关心线程的管理细节,只需将任务投递到线程池即可异步执行。
1.3 线程池的核心优势
| 优势 | 详细说明 |
|---|---|
| 降低系统开销 | 避免了线程频繁创建和销毁带来的 CPU、内存开销,尤其适合短任务场景 |
| 提升响应速度 | 任务到达时直接复用已有线程执行,无需等待线程创建的耗时,大幅降低任务延迟 |
| 控制并发上限 | 限制工作线程的最大数量,避免大量线程抢占 CPU 导致的调度颠簸,保证系统稳定性 |
| 统一线程管理 | 对工作线程进行统一的分配、调优、监控和异常处理,降低业务代码的复杂度 |
1.4 典型应用场景
- 短任务高并发场景:WEB 服务器请求处理、网关接口转发、RPC 调用处理
- 异步非核心任务:日志异步写入、数据统计上报、消息推送
- 批量并行计算:大数据处理、图片 / 视频批量处理、模型推理批量任务
- 低延迟响应服务:对客户端请求延迟敏感的后端服务,如交易系统、即时通讯服务
二. 线程池的核心设计原理:本质是生产者消费者模型
线程池的底层逻辑,就是一个标准的多生产者 - 多消费者模型:
- 生产者:用户线程,向任务队列投递待执行的任务;
- 消费者:线程池内的工作线程,循环从任务队列中获取任务并执行;
- 交易场所:任务队列,是整个模型的核心临界资源,必须保证并发访问的线程安全。


2.1 线程池的核心组成
一个完整的线程池,由四大核心模块构成:
- 任务队列:存储用户投递的待执行任务,通常用队列实现,是线程池的核心临界资源;
- 工作线程组:提前创建的固定数量的工作线程,循环竞争任务队列中的任务执行;
- 同步互斥机制:互斥锁保护任务队列的并发访问,条件变量实现线程的等待与唤醒,解决生产者与消费者的同步问题;
- 线程池状态管理:控制线程池的初始化、运行、停止状态,实现优雅退出,避免任务丢失。


2.2 线程池的核心运行流程
- 初始化阶段:创建固定数量的工作线程,初始化互斥锁、条件变量、任务队列,设置线程池为运行状态;
- 任务投递阶段:用户线程加锁后将任务推入任务队列,若有线程处于等待状态,则唤醒对应的工作线程;
- 任务执行阶段:工作线程循环竞争任务队列,队列为空时进入条件变量休眠;被唤醒后加锁获取任务,解锁后在临界区外执行任务;
- 优雅退出阶段:设置线程池为停止状态,唤醒所有等待的工作线程;工作线程处理完任务队列中剩余的所有任务后,正常退出;主线程等待所有工作线程回收后,释放线程池资源。

2.3 核心设计要点
- 任务执行必须在临界区外:工作线程取到任务后立即释放锁,任务执行是线程私有行为,不占用临界区,最大化提升并发度;
- 条件变量必须用 while 循环判断:防止操作系统的伪唤醒,保证线程被唤醒后一定会重新检查任务队列是否有任务,避免程序异常;
- 优雅退出的双条件判断:只有当「线程池停止运行」且「任务队列为空」时,工作线程才能退出,保证所有已投递的任务都会被执行完毕,不会出现任务丢失;
- 唤醒逻辑优化:只有当有线程处于等待状态时,才发送唤醒信号,避免无效的系统调用,提升程序性能。

三. 手撕线程池:C++ 源码深度解析
我们将基于 Linux 原生 pthread 库,用 C++ 实现工业级线程池,先封装基础的同步互斥组件,再实现线程池核心逻辑,保证代码的可复用性、健壮性和高性能。
3.1 基础组件:RAII 风格的互斥锁与条件变量封装
RAII(资源获取即初始化)是 C++ 管理资源的核心思想,利用对象的生命周期自动管理资源的申请与释放,彻底避免资源泄漏。
3.1.1 互斥锁与锁守卫封装
cpp
#ifndef MUTEX_HPP
#define MUTEX_HPP
#include <iostream>
#include <pthread.h>
/**
* @brief 互斥锁封装类 (The Wrapper Pattern)
* 将原生 pthread_mutex_t 及其相关操作封装进 C++ 类中
* 优点:利用构造/析构函数自动初始化资源,降低直接调用底层接口的心智负担
*/
class Mutex
{
public:
// 构造函数:初始化互斥锁
Mutex()
{
// 这里的 nullptr 表示使用默认的互斥锁属性(非递归、不检测死锁等)
pthread_mutex_init(&_lock, nullptr);
}
// 析构函数:销毁互斥锁
~Mutex()
{
/** * 注意:销毁一个正处于加锁状态或仍有线程在等待的锁会导致未定义行为
* 封装在析构函数中可以确保当 Mutex 对象生命周期结束时,相关资源被内核正确回收
*/
pthread_mutex_destroy(&_lock);
}
// 加锁操作
void Lock()
{
// 若锁已被占用,调用线程将阻塞在此处,进入等待队列
pthread_mutex_lock(&_lock);
}
// 解锁操作
void UnLock()
{
// 唤醒在该互斥锁上等待的线程
pthread_mutex_unlock(&_lock);
}
// 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
// 常见场景:作为 pthread_cond_wait 的参数使用
pthread_mutex_t* Origin()
{
return &_lock;
}
private:
pthread_mutex_t _lock; // POSIX 互斥锁,临界资源访问控制的核心引擎
/** * 补充建议:在实际工程中,通常需要禁用 Mutex 的拷贝构造和赋值
* 因为物理意义上的"锁"在系统中应该是唯一的,不应被克隆。
* Mutex(const Mutex&) = delete;
*/
};
/**
* @brief RAII 风格的锁守卫类 (LockGuard)
* 核心逻辑:Resource Acquisition Is Initialization (资源获取即初始化)
* 作用:解决"忘记解锁"的问题。无论是正常退出、函数 return,还是抛出异常,
* 只要局部变量 LockGuard 出了作用域,其析构函数必然会被调用,从而自动解锁。
*/
class LockGuard
{
public:
// 构造函数:接收一个 Mutex 指针,并立即加锁
// 使用指针是为了能在类外部灵活地管理由同一个 Mutex 保护的不同临界区
LockGuard(Mutex* lockptr) : _lockptr(lockptr)
{
// 实现"一构造就加锁"的自动化语义
_lockptr->Lock();
}
// 析构函数:自动解锁
~LockGuard()
{
// 实现"一销毁就解锁"的自动化语义,保障了代码的异常安全性 (Exception Safety)
_lockptr->UnLock();
}
private:
Mutex* _lockptr; // 维护一个指向 Mutex 的指针,负责在生命周期结束时调用其接口
};
#endif
代码解析:
Mutex类完整封装了 pthread 互斥量的初始化、加锁、解锁、销毁全生命周期,禁用拷贝避免未定义行为;LockGuard是 RAII 的核心实现,利用栈对象的生命周期自动管理锁,彻底避免了手动解锁的遗漏,即使临界区内代码抛出异常,也能保证锁被正确释放。

3.1.2 条件变量封装
cpp
#ifndef COND_HPP
#define COND_HPP
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
/**
* @brief 条件变量封装类
* 核心逻辑:提供线程间的通知机制。
* 它允许线程在某些条件不满足时挂起,并在其他线程改变条件并发送信号时被唤醒。
*/
class Cond
{
public:
// 构造函数:初始化条件变量
Cond()
{
// nullptr 表示使用操作系统默认的条件变量属性
pthread_cond_init(&cond, nullptr);
}
/**
* @brief 等待条件满足
* @param mutex 必须是当前线程已经持有的互斥锁
* * 底层逻辑"三步跳":
* 1. 自动释放传入的 mutex 锁(这样其他线程才能修改临界资源)。
* 2. 将当前线程挂起并加入到该条件变量的等待队列中。
* 3. 当被唤醒返回时,会自动尝试重新竞争并持有该 mutex 锁。
*/
void Wait(Mutex &mutex)
{
// 调用封装好的 Mutex 类的 Origin() 接口,配合底层 C 接口使用
pthread_cond_wait(&cond, mutex.Origin());
}
// 唤醒一个在此条件变量下等待的线程
void NotifyOne()
{
// 唤醒队列中的第一个线程(如果存在)
pthread_cond_signal(&cond);
}
// 唤醒所有在此条件变量下等待的线程
void NotifyAll()
{
// 广播通知,常用于多个消费者或复杂的资源变动场景
pthread_cond_broadcast(&cond);
}
// 析构函数:销毁条件变量资源
~Cond()
{
/**
* 注意事项:
* 销毁一个仍有线程在等待的条件变量是危险行为。
* 在线程池销毁前,通常需要先调用 NotifyAll 并回收所有线程。
*/
pthread_cond_destroy(&cond);
}
private:
pthread_cond_t cond; // POSIX 线程库提供的底层条件变量结构
};
#endif
代码解析:
- 条件变量的核心作用是实现线程间的同步,避免任务队列为空时工作线程 CPU 空转;
Wait函数必须和互斥锁配合使用,因为「条件判断」和「进入等待」必须是原子操作,避免解锁后、等待前信号丢失导致线程永久阻塞;NotifyOne用于任务入队时唤醒单个工作线程,NotifyAll用于线程池退出时唤醒所有等待线程。

3.2 线程池核心类实现
我们用模板类实现线程池,支持任意可调用对象作为任务,先定义任务类型,封装线程再实现线程池核心逻辑。其中我们还使用了日志类,这个我们上一篇刚封装完,并且有点长,这里就不再次展示了。
3.2.1 线程类定义
cpp
#ifndef __THREAD_HPP
#define __THREAD_HPP
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
// 定义线程执行的任务类型,使用包装器增强灵活性
using func_t = std::function<void()>;
// 线程状态枚举:用于构建简单的状态机,确保护法操作
enum class TSTAYUS
{
THREAD_NEW, // 新建状态
THREAD_RUNNING, // 运行状态
THREAD_STOPPED, // 停止/退出状态
};
// 这个是有点bug的:全局静态变量在多线程并发创建对象时存在"竞态条件"
// 多个线程可能同时执行 gunm++,导致线程编号重复,生产环境下建议使用 std::atomic<int>
static int gunm = 1;
class Thread
{
private:
// 获取所属进程的 PID
void get_pid()
{
_pid = getpid();
}
// 获取内核级线程 ID (LWP ID),这才是 Linux 系统监控(如 top -H)看到的真正 ID
void get_lwid()
{
// 原生 pthread 库没有直接获取 LWP 的接口,必须通过系统调用
_lwid = syscall(SYS_gettid);
}
/**
* @brief 静态成员函数作为线程入口点
* 关键逻辑:pthread_create 要求回调函数必须是 void* (*)(void*)
* 类的普通成员函数隐含 this 指针,参数不匹配,故必须设为 static。
* 通过传入 args (this 指针) 重新找回对象上下文。
*/
static void* routine(void* args)
{
Thread* ts = static_cast<Thread*>(args);
ts->get_pid();
ts->get_lwid();
// 为线程设置名字,方便在调试器(如 gdb)中识别
pthread_setname_np(pthread_self(), ts->Name().c_str());
// 执行用户真正传入的任务
ts->_func();
return nullptr;
}
public:
// 构造函数:完成任务绑定与命名,此时线程尚未在内核中创建
Thread(func_t f) : _func(f), _joinable(true), _status(TSTAYUS::THREAD_NEW)
{
_name = "Worker-" + std::to_string(gunm++);
}
// 启动线程:正式调用底层接口
void start()
{
if(_status == TSTAYUS::THREAD_RUNNING)
{
std::cerr << "thread is already running" << std::endl;
return;
}
// 传入 this 作为 routine 的参数,实现 C 到 C++ 的跨越
int n = pthread_create(&_tid, nullptr, routine, this);
if(n != 0)
{
std::cerr << "pthread_create failed" << std::endl;
}
_status = TSTAYUS::THREAD_RUNNING;
}
// 停止线程:通过发送取消请求
void stop()
{
if(_status == TSTAYUS::THREAD_RUNNING)
{
// pthread_cancel 是比较暴力的退出方式,依赖线程内部是否存在取消点
int n = pthread_cancel(_tid);
if(n != 0)
{
std::cerr << "pthread_cancel failed" << std::endl;
}
_status = TSTAYUS::THREAD_STOPPED;
}
else
{
std::cerr << "thread status is : THREAD_STOPPED or THREAD_NEW" << std::endl;
return;
}
}
// 资源回收:阻塞等待线程结束
void join()
{
if(_joinable)
{
// 只有处于 joinable 状态的线程才需要被 join,否则会产生资源泄露
int n = pthread_join(_tid, nullptr);
if(n != 0)
{
std::cerr << "pthread_join failed" << std::endl;
}
printf("lwp: %d, name: %s, join success\n", _lwid, _name.c_str());
}
else {
printf("lwp: %d, name: %s, join failed, because thread is detached\n", _lwid, _name.c_str());
}
}
// 线程分离:将线程设置为由系统自动回收
void detach()
{
if(_joinable && _status == TSTAYUS::THREAD_RUNNING)
{
_joinable = false;
// 分离后,该线程退出时会自动释放所有资源,无需 join
int n = pthread_detach(_tid);
if(n != 0)
{
std::cerr << "pthread_detach failed" << std::endl;
}
}
}
// 获取线程名称接口
std::string Name()
{
return _name;
}
~Thread()
{
// 析构函数中未做强制 join,这是为了给使用者留出控制权
// 但要注意,如果对象销毁时线程还在跑且未 detach,可能会导致程序崩溃
}
private:
pthread_t _tid; // 线程库层面的 ID (用户层 ID)
pid_t _pid; // 所属进程 ID
pid_t _lwid; // 轻量级进程 ID (内核层真正的线程 ID)
std::string _name; // 线程可读性名称
func_t _func; // 线程执行的任务包装器
bool _joinable; // 是否允许被等待标记
TSTAYUS _status; // 当前线程状态机
};
#endif

3.2.2 任务类型定义
cpp
#pragma once
#include <iostream>
#include <functional>
#include <pthread.h>
#include "Logger.hpp"
/**
* @brief 任务类型包装器
* 使用 std::function 实现类型擦除 (Type Erasure)
* 优点:线程池不需要知道具体任务的细节,只要是"无参无返回值"的调用对象(函数指针、Lambda、仿函数)
* 都可以被封装进 task_t,这极大增强了线程池的通用性。
*/
using task_t = std::function<void()>;
using namespace LogModule;
// 1. 全局函数
/**
* @brief 示例任务1:模拟 IO/打印型任务
* 重点在于展示如何在任务内部识别当前正在干活的线程。
*/
void task1()
{
char name[64];
// pthread_getname_np 是 Linux 特有的接口,用于获取线程的别名(在 Thread.hpp 中通过 pthread_setname_np 设置)
// 这在多线程调试时非常关键,能帮你确定任务是否在预期的 Worker 线程中执行。
pthread_getname_np(pthread_self(), name, sizeof(name));
LOG(LogLevel::DEBUG) << "执行任务1: 打印消息 |" << name << "|";
}
/**
* @brief 示例任务2:模拟计算型任务
* 模拟一个简单的算术逻辑处理。
*/
void task2()
{
char name[64];
// 每一个任务被执行时,实际上都是在某个 Worker 线程的调用栈中运行。
pthread_getname_np(pthread_self(), name, sizeof(name));
LOG(LogLevel::DEBUG) << "执行任务2: 计算 1+1 = " << 1 + 1 << " |" << name << "|";
}

3.2.3 线程池核心类框架(大致接口有那些)
cpp
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
/**
* @brief 线程池核心类 (基于生产者-消费者模型设计)
* 采用模板类 T,以支持不同类型的任务逻辑(通常为 std::function<void()>)
*/
template<typename T>
class ThreadPool
{
private:
// 【私有接口:内部逻辑支撑】
// 检查任务队列是否为空
bool IsEmptyQueue();
// 辅助函数:从队列中获取一个任务(封装 pop 动作)
T PopHelper();
/**
* @brief 线程执行流入口 (核心死循环)
* 内部包含:加锁、条件变量等待、任务获取、任务执行、状态检测
*/
void ThreadRoutine();
public:
// 【公有接口:外部操作指南】
/**
* @brief 构造函数
* @param num 预创线程的数量,默认为 5
* 职责:初始化成员变量,并预分配 Thread 对象
*/
ThreadPool(int num = 5);
/**
* @brief 启动线程池
* 职责:将状态改为运行中,并真正调用每个 Thread 的 start() 方法创建内核线程
*/
void Start();
/**
* @brief 生产者接口:提交任务
* @param task 待执行的任务对象
* 职责:加锁入队,并唤醒(Notify)正在休眠的消费者线程
*/
void Enqueue(const T& task);
/**
* @brief 温和关闭线程池
* 职责:修改运行状态,并广播(NotifyAll)所有线程,确保积压任务处理完后线程能正常退出
*/
void Stop();
/**
* @brief 资源回收接口
* 职责:循环调用线程对象的 join(),确保主线程在子线程彻底回收后再退出
*/
void Wait();
// 析构函数
~ThreadPool();
private:
// 【核心资源:状态与同步控制】
std::vector<Thread> _threads; // 线程"工人"管理数组
int _num; // 预设线程规模
bool _isrunning; // 运行状态标识位(核心状态机)
int _sleeper_cnt; // 统计当前处于 Wait 状态的线程数
std::queue<T> _queue; // 任务队列:充当生产者与消费者之间的"交易场所"
Mutex _mutex; // 互斥锁:保证队列操作的原子性
Cond _cond; // 条件变量:实现线程间的同步通知
};
#endif
成员变量解析:
_queue:任务队列,是生产者和消费者的核心共享资源,所有访问必须加锁保护;_mutex:互斥锁,保护任务队列、_sleeper_cnt、_isrunning所有共享资源的并发访问;_sleeper_cnt:记录等待线程数量,用于优化唤醒逻辑,只有当有线程等待时才发送唤醒信号,避免无效系统调用;_isrunning:线程池运行状态标志,控制工作线程的运行与退出,实现优雅关闭。
3.2.4 核心成员函数实现

线程池初始化与启动
cpp
public:
/**
* @brief 线程池构造函数
* @param num 指定线程池中初始线程的数量
* 职责:完成成员变量初始化,并预建 Thread 对象容器。
*/
ThreadPool(int num = gDefaultCnt): _num(num), _isrunning(false), _sleeper_cnt(0)
{
for(int i = 0; i < num; i++)
{
// 利用lambda表达式捕捉this指针,不然会出现参数不匹配的问题
// 跟Thread.hpp中有关系
/**
* 深度解析:
* 1. 桥接作用:Thread 类期待一个 func_t (void()),而 ThreadRoutine 是成员函数。
* 2. 闭包特性:通过 [this] 捕获当前对象的地址,使得 lambda 体内可以访问私有成员 ThreadRoutine。
* 3. 性能优化:emplace_back 直接在 vector 内存中构造 Thread 对象,避免了额外的拷贝或移动开销。
*/
_threads.emplace_back([this](){
this->ThreadRoutine();
});
}
}
/**
* @brief 启动线程池
* 职责:将池子状态设为运行,并让底层的每一个 pthread 真正跑起来。
*/
void Start()
{
// 使用 LockGuard 确保 Start 操作的原子性
// 防止在多线程环境下该线程池被多次重复调用 Start() 导致逻辑混乱
LockGuard lockGuard(&_mutex);
// 幂等性检查:如果当前处于运行状态,直接返回,确保 Start 只能成功执行一次
if(_isrunning)
return;
// 状态翻转:标记池子已进入服务状态
_isrunning = true;
// 遍历管理容器,逐个调用 Thread 类的 start() 封装,触发 pthread_create
for(auto& thread: _threads)
thread.start();
// 启发:此时所有子线程将竞相进入 ThreadRoutine 的 while(true) 循环
}
代码解析:
- 类的非静态成员函数有隐式的 this 指针,无法直接作为 pthread 的回调函数,因此用 lambda 表达式捕获 this 指针,将类实例传入回调,再调用成员函数;
- 构造函数内仅创建线程对象,
Start设置运行状态,分离初始化和启动逻辑,方便线程池的生命周期管理。

工作线程核心例程(线程池灵魂)
cpp
private:
/**
* @brief 检查队列状态
* 注意:该函数虽为私有,但被 ThreadRoutine 调用时必须处于互斥锁的保护下。
*/
bool IsEmptyQueue()
{
return _queue.empty();
}
/**
* @brief 提取任务辅助函数
* 职责:封装从 STL 队列中获取并移除任务的动作。
* 底层细节:STL 容器非线程安全,调用此函数前必须确保当前线程已持有锁。
*/
T PopHelper()
{
T t = _queue.front();
_queue.pop();
return t;
}
/**
* @brief 线程的核心执行回路 (Worker Loop)
* 每个线程启动后,都会在这个 while(true) 中度过余生,直到池子关闭。
*/
void ThreadRoutine()
{
char name[64];
// 获取线程名,用于日志输出,方便追踪是哪个"工人"在干活
pthread_getname_np(pthread_self(), name, sizeof(name));
while(true)
{
T task; // 定义局部任务对象,用于从队列中拷贝任务到本地执行流
// 临界区作用域开始:保证对任务队列的操作是原子的
{
LockGuard lockGuard(&_mutex); // 加锁保护,RAII 机制确保出了这个花括号自动解锁
// 1. 任务队列为空 && 线程处于运行状态(不退出) -- 允许休眠
/**
* 深度解析:
* 为什么要用 while 而不是 if?
* 答:为了应对"虚假唤醒"。即便被唤醒,醒来第一件事必须是再次检查条件,
* 确保真的有任务可领,否则继续睡。
*/
while(IsEmptyQueue() && _isrunning) // 防止伪唤醒
{
LOG(LogLevel::DEBUG) << "没有任务,线程休眠: " << "|" << name << "|";
_sleeper_cnt++; // 进入休眠状态前,计数器自增
_cond.Wait(_mutex); // 核心动作:原子解锁并挂起;被唤醒后自动重新加锁
_sleeper_cnt--; // 被唤醒后,计数器自减
LOG(LogLevel::DEBUG) << "有任务,线程唤醒: " << "|" << name << "|";
}
// 2. 任务队列为空 && 线程不处于运行状态(要退出) -- 允许退出
/**
* 优雅退出的关键判断:
* 只有当"不想跑了"且"活儿都干完了",线程才 break 跳出循环。
* 这样保证了即使调用了 Stop,队列里的存量任务依然能被处理完。
*/
if(IsEmptyQueue() && !_isrunning)
{
LOG(LogLevel::INFO) << "Thread: " << name << " quit";
break;
}
// 3. 任务队列不为空 && 线程处于运行状态(不退出) -- 要先处理完任务
// 任务队列不为空 && 线程不处于运行状态(要退出) -- 要先处理完任务
// 到这里了肯定是有任务:执行真正的"领任务"动作
task = PopHelper();
} // 临界区作用域结束,lockGuard 析构,释放互斥锁
/**
* 核心性能考量点:
* task() 任务处理放在临界区外面来执行。
* 理由:任务执行通常很耗时,如果持锁执行,其他线程将无法领任务,
* 整个线程池将退化为串行执行。释放锁后再处理,才是真正的并发。
*/
task();
}
}
核心设计深度解析:
- while 循环防伪唤醒:操作系统可能会无故唤醒等待的线程(伪唤醒),用 while 循环会在唤醒后重新检查任务队列是否有任务,不满足则继续等待,保证程序健壮性,这是 pthread 条件变量的标准使用规范;
- 优雅退出双条件判断 :只有当「线程池停止」且「任务队列为空」时,线程才会退出,保证所有已投递的任务都会被执行完毕,绝对不能用
pthread_cancel强制终止线程,会导致任务执行中断、资源泄漏; - 任务执行在临界区外 :取出任务后,锁会在离开作用域时自动释放,耗时的任务执行完全不占用临界区,其他线程可以正常投递和获取任务,最大化并发度,这是线程池高性能的核心设计。


任务投递接口
cpp
/**
* @brief 生产者接口:下发任务
* @param task 外部提交的任务对象(通常是一个回调包装器)
* 职责:将任务推入队列,并按需唤醒等待中的"工人"线程。
*/
void Enqueue(const T& task)
{
// 1. 加锁保护:任务队列 (_queue) 是临界资源,必须保证 push 操作的原子性
LockGuard lockGuard(&_mutex);
// 2. 状态判定:这不仅是逻辑检查,更是安全防线
if(!_isrunning) // 如果当前处于停止状态,禁止继续加任务
return;
// 3. 任务入队:将任务拷贝/移动到 STL 队列中
_queue.push(task);
// 4. 唤醒机制:
// 唤醒一个来线程来执行任务
/**
* 性能优化点:按需通知 (Selective Notification)
* 只有当确实有线程在条件变量下挂起 (_sleeper_cnt > 0) 时,才调用 NotifyOne。
* 理由:如果所有线程都在忙碌处理任务,调用 Notify 会产生无谓的内核系统调用开销。
*/
if(_sleeper_cnt > 0)
_cond.NotifyOne();
// 进阶思考注释:
// if(_sleeper_cnt > 0 && _queue.size() > _num)
// _cond.NotifyOne();
/**
* 关于你注释掉的这两行:
* 这通常用于"批量唤醒"或"负载调节"尝试。
* 在高并发场景下,有时候会积压一定数量的任务后再统一唤醒,或者根据队列长度决定唤醒频率,
* 但对于基础线程池,目前的 NotifyOne 已经能保证最优的实时响应。
*/
}
代码解析:
- 任务队列是临界资源,投递任务必须加锁,保证多线程并发投递的线程安全;
- 线程池停止后禁止投递新任务,避免任务入队后线程已退出导致任务丢失;
- 仅当有线程处于等待状态时才发送唤醒信号,避免无意义的系统调用,提升性能。


程池停止与资源回收
cpp
/**
* @brief 优雅关闭接口 (Graceful Shutdown)
* 职责:向所有执行线程发布"下班"信号,并确保已有的存量任务有机会被处理。
*/
void Stop()
{
// 加锁进入临界区,修改状态位和发送通知必须是原子的,防止错失信号
LockGuard lockGuard(&_mutex);
// 处于运行状态才有停止的必要
if(_isrunning)
{
LOG(LogLevel::DEBUG) << "关闭线程池";
// 状态翻转:这是逻辑上的"关门",Enqueue 接口将不再接受新任务
_isrunning = false; // 将状态改为false;
// 唤醒所有的去执行,因为可能停止了但是任务还没做完
/**
* 深度解析:
* 为什么是 NotifyAll 而不是 NotifyOne?
* 答:此时所有在 Wait 队列中的线程都必须醒来检查 _isrunning 状态。
* 只有全部唤醒,它们才能意识到池子已经关闭,从而打破 while 循环走向退出路径。
*/
if(_sleeper_cnt > 0)
_cond.NotifyAll();
// 这样的做法不好
// for(auto& thread: _threads)
// thread.stop();
/**
* 补充注释:
* 为什么手动调用 thread.stop (pthread_cancel) 不好?
* 1. 暴力中断:可能导致线程正在处理的任务执行一半被强杀,造成数据不一致。
* 2. 资源泄露:如果线程持有某些非 RAII 资源(如堆内存、文件描述符),直接 cancel 会导致这些资源无法释放。
* 3. 阻塞风险:cancel 依赖取消点,不一定能立即见效。
*/
}
}
/**
* @brief 线程回收接口
* 职责:主执行流阻塞等待所有子线程干完活并彻底退出。
*/
void Wait()
{
// 这里就不加锁了,防止阻塞
/**
* 架构逻辑:
* Wait() 通常紧随 Stop() 之后。
* 不加锁的原因:pthread_join 本身就是阻塞式等待。如果此时持锁等待线程退出,
* 而子线程在退出逻辑中恰好也需要这把锁(例如 ThreadRoutine 里的判断),就会产生死锁。
*/
for(auto& thread: _threads)
thread.join();
}
/**
* @brief 析构函数
* 在这个版本中为空,因为资源的释放逻辑被显式地放在了 Stop 和 Wait 中。
*/
~ThreadPool()
{
/**
* 生产环境建议:
* 可以在析构函数中检查 _isrunning。
* 如果用户忘记调用 Stop/Wait,析构函数应主动介入,防止产生"僵尸线程"或对象销毁后的野指针访问。
*/
}
代码解析:
Stop函数修改线程池运行状态后,必须用NotifyAll唤醒所有等待的线程,让所有线程都能检查退出条件,避免部分线程永久休眠;Wait函数循环调用pthread_join等待所有工作线程退出,保证主线程不会提前终止,导致进程退出、任务未执行完毕。


- 我们没使用图中这种
Wait()调用的方式,后面可以在单例模式中尝试一下
3.3 线程池使用示例(ThreadPool_v1完整版)
Threadpool.hpp
cpp
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP
// 可以看到我们直接使用了很多之前自己造的轮子
#include <iostream>
#include <pthread.h>
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace LogModule;
// 默认线程池大小,通常根据 CPU 核心数进行调整
const static int gDefaultCnt = 5;
/**
* @brief 通用线程池模板类
* @tparam T 任务类型,通常是一个可调用对象(如 std::function)
*/
template<typename T>
class ThreadPool
{
private:
// 内部检查工具:判断队列是否为空,需在加锁环境下调用
bool IsEmptyQueue()
{
return _queue.empty();
}
// 内部辅助工具:封装出队动作,减少主循环中的代码冗余
T PopHelper()
{
T t = _queue.front();
_queue.pop();
return t;
}
/**
* @brief 线程的核心工作循环 (Worker Routine)
* 每个线程启动后都会陷入此函数的死循环中,直到池子关闭
*/
void ThreadRoutine()
{
char name[64];
// 获取线程名称,用于区分不同的 Worker 日志输出
pthread_getname_np(pthread_self(), name, sizeof(name));
while(true)
{
T task; // 线程本地的任务包装器
// 临界区作用域:确保锁的粒度尽可能小,仅保护对共享队列的操作
{
LockGuard lockGuard(&_mutex); // RAII 自动加锁保护
// 1. 任务队列为空 && 线程处于运行状态(不退出) -- 允许休眠
/**
* 关键点:为什么用 while 而不是 if?
* 答:为了应对"虚假唤醒 (Spurious Wakeup)"。线程被唤醒后必须重新
* 检查条件,确保队列里真的有数据,否则必须继续休眠。
*/
while(IsEmptyQueue() && _isrunning) // 防止伪唤醒
{
LOG(LogLevel::DEBUG) << "没有任务,线程休眠: " << "|" << name << "|";
_sleeper_cnt++; // 记录进入休眠的线程数,优化生产者的通知策略
_cond.Wait(_mutex); // 核心动作:释放锁 -> 挂起 -> 被唤醒 -> 重新竞争锁
_sleeper_cnt--; // 被唤醒并抢到锁后,计数自减
LOG(LogLevel::DEBUG) << "有任务,线程唤醒: " << "|" << name << "|";
}
// 2. 任务队列为空 && 线程不处于运行状态(要退出) -- 允许退出
/**
* 优雅退出的逻辑:
* 只有当"池子关门"且"存量活儿干完"时,线程才正式退出。
* 如果队列还有任务,即使 _isrunning 为 false,也会走到下面的 PopHelper 继续干活。
*/
if(IsEmptyQueue() && !_isrunning)
{
LOG(LogLevel::INFO) << "Thread: " << name << " quit";
break;
}
// 3. 任务队列不为空 && 线程处于运行状态(不退出) -- 要先处理完任务
// 任务队列不为空 && 线程不处于运行状态(要退出) -- 要先处理完任务
// 到这里了肯定是有任务
task = PopHelper();
} // 临界区结束:LockGuard 析构,释放互斥锁
/**
* 性能优化的核心:
* task() 的处理放在临界区外部。
* 理由:任务执行通常很耗时,如果持锁运行,线程池会退化为单线程。
* 让其他线程能在该任务执行期间去竞争锁领新任务,实现真正的并发。
*/
task();
}
}
public:
/**
* @brief 构造函数:初始化管理资源
* 注意:此时内核线程尚未真正创建,只是在容器中预置了 Thread 对象
*/
ThreadPool(int num = gDefaultCnt): _num(num), _isrunning(false), _sleeper_cnt(0)
{
for(int i = 0; i < num; i++)
{
// 利用lambda表达式捕捉this指针,不然会出现参数不匹配的问题
// 跟Thread.hpp中有关系:将成员函数 ThreadRoutine 转换为 func_t 类型
_threads.emplace_back([this](){
this->ThreadRoutine();
});
}
}
/**
* @brief 启动线程池服务
* 职责:开启控制开关,并逐一触发线程创建
*/
void Start()
{
LockGuard lockGuard(&_mutex);
// 幂等性保护:防止线程池被多次重复 Start
if(_isrunning)
return;
_isrunning = true; // 翻转运行状态
for(auto& thread: _threads)
thread.start(); // 封装了 pthread_create
}
/**
* @brief 生产者接口:将任务加入队列
*/
void Enqueue(const T& task)
{
LockGuard lockGuard(&_mutex);
if(!_isrunning) // 如果当前处于停止状态,禁止继续加任务(拒绝服务策略)
return;
_queue.push(task);
/**
* 唤醒策略优化:
* 唤醒一个来线程来执行任务。
* 只有当确实有线程在睡觉时才发信号,减少无效的内核系统调用开销。
*/
if(_sleeper_cnt > 0)
_cond.NotifyOne();
// 进阶优化提示:如果任务堆积过多,可以考虑 NotifyAll 或 动态增加线程
// if(_sleeper_cnt > 0 && _queue.size() > _num)
// _cond.NotifyOne();
}
/**
* @brief 停止线程池服务 (优雅停止)
*/
void Stop()
{
LockGuard lockGuard(&_mutex);
// 处于运行状态才有停止的必要
if(_isrunning)
{
LOG(LogLevel::DEBUG) << "关闭线程池";
_isrunning = false; // 1. 先修改状态位,切断 Enqueue 的入口
// 2. 唤醒所有正在休眠的线程。
// 它们醒来后会因为 _isrunning 为 false 且队列为空而 break。
if(_sleeper_cnt > 0)
_cond.NotifyAll();
// 这样的做法不好:直接 cancel 会导致任务处理一半被强杀,产生不可控后果
// for(auto& thread: _threads)
// thread.stop();
}
}
/**
* @brief 等待线程回收
*/
void Wait()
{
// 这里不加锁:pthread_join 本身是阻塞的。
// 如果在此加锁,会导致正在尝试退出的子线程因为竞争不到锁而无法完成逻辑。
for(auto& thread: _threads)
thread.join();
}
~ThreadPool()
{}
private:
// 线程管理资源
std::vector<Thread> _threads; // 管理线程对象的容器
int _num; // 线程池设定的初始线程规模
bool _isrunning; // 线程池存活状态标识
// int _status; // 进阶:可标识 RUNNING, PAUSE, STOPPED 等精细状态
int _sleeper_cnt; // 实时记录正在等待条件变量的空闲线程数
// 生产/消费的核心组件
std::queue<T> _queue; // 任务缓冲池
Mutex _mutex; // 保护队列的互斥锁
Cond _cond; // 协调生产者与消费者的条件变量
};
#endif

Main.cc
cpp
#include "Logger.hpp"
#include "Task.hpp"
#include "Threadpool.hpp"
#include <memory>
#include <unistd.h>
/**
* @brief 线程池应用示例 (The Driver Program)
* 职责:作为"生产者"线程,负责初始化环境、下发任务并控制整体生命周期。
*/
int main()
{
// 0. 初始化日志系统:开启控制台输出策略,这是我们观察多线程并发行为的"眼睛"
ENABLE_CONSOLE_LOG_STRATEGY();
// 1. 创建线程池对象:
// 使用 std::unique_ptr 管理线程池,体现了现代 C++ 的 RAII 资源管理思想
// task_t 是在 Task.hpp 中定义的 std::function<void()> 类型擦除包装器
std::unique_ptr<ThreadPool<task_t>> tp = std::make_unique<ThreadPool<task_t>>();
// 2. 启动线程池:
// 此时底层会真正创建 5 个(默认值)Worker 线程,并让它们进入空闲休眠状态,等待任务
tp->Start();
// 3. 生产过程:主线程充当生产者角色
int cnt = 10;
while(cnt--)
{
// 打印当前循环状态,方便追踪生产进度
LOG(LogLevel::DEBUG) << "-----------------------: " << cnt;
// 模拟生产间隔:每秒投放一个任务,让日志打印不至于瞬间刷屏,方便观察
sleep(1);
// 向线程池投喂任务1:打印消息任务
// Enqueue 会自动唤醒一个正在休眠的 Worker 线程来处理
tp->Enqueue(task1);
sleep(1);
// 向线程池投喂任务2:计算任务
tp->Enqueue(task2);
}
/**
* 4. 优雅停机协议:
* 这是最能体现代码鲁棒性的地方
*/
// 发出停止指令:
// 将池子的 _isrunning 设为 false,并广播唤醒所有休眠线程。
// 注意:此时队列里可能还有没做完的任务,线程会坚持把活儿干完再退出。
tp->Stop();
// 等待回收:
// 主线程阻塞于此,直到所有 Worker 线程处理完残余任务并正常 join。
// 这保证了程序退出时,没有任何"僵尸执行流"存在。
tp->Wait();
// unique_ptr 离开作用域,自动析构 ThreadPool 对象,内存安全释放。
return 0;
}




四. 进阶优化:线程安全的单例模式线程池(ThreadPool_v2)
在实际的后端开发中,线程池通常是进程内全局唯一的资源,需要用单例模式保证整个程序中只有一个线程池实例,避免资源浪费和管理混乱。
4.1 单例模式的核心要求
- 构造函数私有化,外部无法直接创建对象;
- 禁用拷贝构造和赋值运算符,防止对象拷贝破坏单例;
- 提供全局唯一的实例获取接口,保证实例只被创建一次;
- 多线程环境下保证线程安全,避免并发创建多个实例。

4.2 饿汉模式 vs 懒汉模式
• 饿汉模式 :吃完饭立刻洗碗,程序启动时就创建实例,用的时候直接拿。优点是实现简单、天然线程安全;缺点是实例初始化耗时时会拖慢程序启动速度,即使不用也会占用资源。
• 懒汉模式 :吃完饭先不洗碗,下一顿用的时候再洗,核心是延时加载,第一次使用时才创建实例。优点是不影响程序启动速度,按需加载;缺点是多线程环境下需要解决线程安全问题。
工业级开发中,懒汉模式的使用更广泛,它不会影响服务的启动速度,符合后端服务的设计规范。


4.3 线程安全的懒汉单例线程池(双检锁 DCL)
双检锁(Double-Check Locking, DCL)是工业界最常用的线程安全懒汉单例实现,完美平衡了安全性和性能。旧版是一个"随用随建的任务工具",单例版是一个"全局唯一的任务调度中心"
cpp
#ifndef THREADPOOL_HPP
#define THREADPOOL_HPP
// 可以看到我们直接使用了很多之前自己造的轮子
#include <iostream>
#include <memory>
#include <pthread.h>
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace LogModule;
const static int gDefaultCnt = 5;
/**
* @brief 线程池单例模板类
* 采用了"懒汉模式"实现,即在第一次调用 GetInstance 时才进行实例化。
*/
template<typename T>
class ThreadPool
{
private:
// 内部逻辑:判定队列状态
bool IsEmptyQueue()
{
return _queue.empty();
}
// 内部逻辑:原子化提取任务(调用前需持有锁)
T PopHelper()
{
T t = _queue.front();
_queue.pop();
return t;
}
/**
* @brief 消费者核心执行流
* 运行于子线程栈中,通过条件变量实现高效的任务等待与唤醒。
*/
void ThreadRoutine()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof(name));
while(true)
{
T task; // 任务对象
// 临界区作用域:确保锁的持有时间最短化
{
LockGuard lockGuard(&_mutex); // 加锁保护
// 1. 任务队列为空 && 线程处于运行状态(不退出) -- 允许休眠
/**
* 深度解析:
* 此处的 while 循环不仅解决了"虚假唤醒",还配合单例模式
* 确保了多个子线程在竞争唯一任务队列时的逻辑严密性。
*/
while(IsEmptyQueue() && _isrunning) // 防止伪唤醒
{
LOG(LogLevel::DEBUG) << "没有任务,线程休眠: " << "|" << name << "|";
_sleeper_cnt++;
_cond.Wait(_mutex); // 核心:释放锁 -> 挂起 -> 被唤醒 -> 重获锁
_sleeper_cnt--;
LOG(LogLevel::DEBUG) << "有任务,线程唤醒: " << "|" << name << "|";
}
// 2. 任务队列为空 && 线程不处于运行状态(要退出) -- 允许退出
if(IsEmptyQueue() && !_isrunning)
{
LOG(LogLevel::INFO) << "Thread: " << name << "quit";
break;
}
// 3. 任务队列不为空,无论运行状态如何,都要提取任务处理
task = PopHelper();
}
// 任务执行放在锁外,这是实现真正并发、避免线程池退化为单线程的关键
task();
}
}
// 单例模式防御:私有化构造函数,杜绝外部随意创建对象
private:
ThreadPool(int num = gDefaultCnt): _num(num), _isrunning(false), _sleeper_cnt(0)
{
for(int i = 0; i < num; i++)
{
// 利用lambda表达式捕捉this指针,不然会出现参数不匹配的问题
// 跟Thread.hpp中有关系
_threads.emplace_back([this](){
this->ThreadRoutine();
});
}
}
// 将拷贝和赋值语句去掉
/**
* 补充建议:
* 虽然这里用了 = default,但在标准的单例模式中,
* 拷贝构造和赋值运算符通常应该设为 = delete,以防止实例被"克隆"。
*/
ThreadPool(const ThreadPool<T>& ) = default;
ThreadPool<T>& operator =(const ThreadPool<T>&) = default;
public:
// 定义成静态的:全局唯一访问点
/**
* @brief 获取单例对象的静态接口
* 采用了"双检查锁 (Double-Checked Locking)"机制。
*/
static ThreadPool<T>* GetInstance()
{
// 第一层判断:为了提高性能。如果实例已存在,直接返回,避免不必要的加锁开销。
if(_instance == nullptr)
{
// 加锁:保证创建实例过程的原子性,防止多个线程同时执行 new 操作
LockGuard lockGuard(&_signalton_lock);
// 第二层判断:为了保证唯一性。在获得锁后再次检查,
// 确认在此期间没有其他线程提前创建了实例。
if(_instance == nullptr)
{
LOG(LogLevel::DEBUG) << "首次创建,创建成功" ;
_instance = new ThreadPool<T>(); // 只会创建一次
}
}
return _instance;
}
// 启动线程服务
void Start()
{
LockGuard lockGuard(&_mutex);
if(_isrunning)
return;
_isrunning = true;
for(auto& thread: _threads)
thread.start();
}
// 生产者下发任务
void Enqueue(const T& task)
{
LockGuard lockGuard(&_mutex);
if(!_isrunning) // 如果当前处于停止状态,禁止继续加任务
return;
_queue.push(task);
// 唤醒一个来线程来执行任务
// 优化策略:只有存在正在睡觉的工人才发通知
if(_sleeper_cnt > 0)
_cond.NotifyOne();
}
// 优雅停止线程池
void Stop()
{
LockGuard lockGuard(&_mutex);
if(_isrunning)
{
LOG(LogLevel::DEBUG) << "关闭线程池";
_isrunning = false; // 改变状态,作为 ThreadRoutine 退出的触发信号
// 唤醒所有的去执行, 保证所有线程都能意识到状态改变并正确 break
if(_sleeper_cnt > 0)
_cond.NotifyAll();
}
}
// 阻塞式资源回收
void Wait()
{
// join 操作本身阻塞,且不涉及临界资源修改,故无需加锁
for(auto& thread: _threads)
thread.join();
}
~ThreadPool()
{}
private:
// 线程池管理组件
std::vector<Thread> _threads; // 管理线程对象的容器
int _num; // 线程池规模
bool _isrunning; // 全局生命周期开关
int _sleeper_cnt; // 记录当前空闲工人的数量
std::queue<T> _queue; // 共享任务队列
Mutex _mutex; // 保护任务队列的互斥锁
Cond _cond; // 协调生产/消费节奏的条件变量
// 单例模式静态成员
static ThreadPool<T> *_instance; // 全局唯一实例指针
static Mutex _signalton_lock; // 保护单例实例化的静态锁
};
// 静态成员变量在类外初始化:
// 静态指针在 main 运行前初始化为 null,保证 GetInstance 的逻辑起点正确。
template<typename T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;
template <typename T>
Mutex ThreadPool<T>::_signalton_lock;
#endif
双检锁核心设计解析:
- 第一重 if 判断:实例创建完成后,所有获取实例的操作都不会进入加锁逻辑,直接返回实例,避免了每次获取实例都加锁的性能开销,这是双检锁的核心优化点;
- 加锁保护:只有实例为空时才会加锁,保证同一时间只有一个线程能进入实例创建代码块;\
- 第二重 if 判断:防止多个线程同时通过第一重 if 判断,比如线程 A 和 B 同时判断实例为空,A 先拿到锁创建了实例,B 拿到锁后如果没有第二重判断,会再次创建实例,破坏单例模式;
- 注意事项 :C++11 之前需要给
_instance加上volatile关键字,防止编译器指令重排导致实例未初始化完成就被使用;C++11 及之后,静态局部变量的初始化是天然线程安全的,还有更简洁的单例实现方式。


4.4 单例线程池使用示例
cpp
#include "Logger.hpp"
#include "Task.hpp"
#include "Threadpool.hpp"
#include <memory>
#include <unistd.h>
/**
* @brief 单例线程池实战演示
* 核心变化:从"局部管理"变为"全局单例"。
* 这种模式下,程序的任何角落都可以通过 GetInstance() 随时随地提交任务。
*/
int main()
{
// 初始化日志配置
ENABLE_CONSOLE_LOG_STRATEGY();
// std::unique_ptr<ThreadPool<task_t>> tp = std::make_unique<ThreadPool<task_t>>(); // 这个就不行了
/**
* @note 为什么 unique_ptr/make_unique 不行了?
* 答:因为在 ThreadPool_v2 中,我们将构造函数设为了 private。
* make_unique 内部需要调用 new 来触发构造函数,而外部没有访问权限。
* 这正是单例模式的"护城河",防止了程序员在外部不小心创建出第二个池子。
*/
// ThreadPool<task_t>::GetInstance()->Start();
/**
* @brief 获取全局唯一实例
* 第一次调用时,会在堆上申请内存并初始化;
* 后续调用直接返回同一个对象的指针,确保全局只有一套任务队列和线程组。
*/
auto tp = ThreadPool<task_t>::GetInstance();
// 启动线程池:让预创的线程进入待命状态
tp->Start();
// 模拟生产者行为
int cnt = 10;
while(cnt--)
{
// 打印分界线,标识每一次生产循环
LOG(LogLevel::DEBUG) << "-----------------------: " << cnt;
// 间隔 1 秒投喂任务,模拟低频持续的业务流量
sleep(1);
tp->Enqueue(task1); // 派发打印任务
sleep(1);
tp->Enqueue(task2); // 派发计算任务
}
/**
* @brief 优雅停机
* 即使是单例,在主流程结束前也建议显式调用 Stop 和 Wait。
* Stop():阻止新任务进入,并唤醒所有 Worker 准备下班。
* Wait():主线程在此等待,确保 Worker 线程处理完队列里的"存量工作"后被安全 join。
*/
tp->Stop();
tp->Wait();
return 0;
}
- 可以看到首次创建成功,剩下的其实跟之前的输出结果看不出啥区别



补充和优化

五. 线程池背后的核心安全问题
线程池的底层是多线程的同步与互斥,只有彻底理解线程安全、死锁等核心问题,才能写出健壮的高并发代码。
5.1 线程安全与函数可重入

核心概念
- 线程安全:多个线程并发访问共享资源时,程序能正确执行,不会出现数据竞争、结果异常,就称这个程序 / 函数是线程安全的。
- 可重入:同一个函数被不同的执行流调用,前一个调用还未执行完,就有其他执行流再次进入,运行结果不会出现任何问题,这个函数就是可重入函数。

联系与区别
- 可重入函数一定是线程安全的,线程安全的函数不一定是可重入的;
- 线程安全描述的是多线程并发访问的运行特性,可重入描述的是函数被重复调用的代码特性;
- 函数不可重入,大概率会导致线程安全问题。


常见不安全场景
- 不保护共享全局 / 静态变量的函数;
- 调用了 malloc/free、标准 I/O 库函数的函数(内部使用全局数据结构,不可重入);
- 返回静态变量指针的函数;
- 函数状态随调用发生变化的函数。
5.2 死锁:多线程编程的头号杀手
死锁是指一组线程各自持有不会释放的资源,又互相申请对方持有的资源,导致所有线程永久阻塞等待的状态。


死锁的四个必要条件
死锁发生时,这四个条件必须同时满足,破坏其中任意一个,就能避免死锁:
-
互斥条件:一个资源同一时间只能被一个线程使用,锁的基本特性,无法破坏;
-
请求与保持条件 :线程申请新资源阻塞时,不释放已经持有的资源;

-
不剥夺条件 :线程已持有的资源,在使用完之前不能被其他线程强行剥夺;

-
循环等待条件 :多个线程形成头尾相接的循环等待资源的关系。

避免死锁的核心方法(破坏上面的四个条件中任意一个即可,互斥最简单,不用就行。其他的有的我没详细说,大概看看了解一下,面试问的比较少,可能会问)
- 破坏循环等待条件 :最常用的方式,保证所有线程加锁顺序完全一致;一次性申请所有需要的资源;用
std::lock一次性锁定多个互斥锁; - 破坏请求与保持条件 :申请锁失败时,立即释放已持有的所有锁,使用非阻塞的
trylock接口; - 代码规范:使用 RAII 风格的锁管理,避免忘记解锁;避免临界区内嵌套加锁;避免临界区内执行耗时操作、调用阻塞函数。


5.3 STL 容器与智能指针的线程安全
这是 C++ 多线程编程中最容易踩坑的点:
- STL 容器默认不是线程安全的:STL 的设计初衷是极致的性能,加锁会带来巨大的性能开销,因此所有 STL 容器(vector、queue、map 等)都不是线程安全的。多线程并发读写同一个容器时,必须由开发者自行加锁保护,否则会出现迭代器失效、数据损坏、程序崩溃等问题。
- 智能指针的线程安全
unique_ptr:所有权唯一,只在当前代码块内生效,天然线程安全;shared_ptr:标准库用原子操作 (CAS) 保证了引用计数的增减是原子的,因此引用计数操作是线程安全的;但指向的对象的并发访问,不是线程安全的,需要自行加锁保护。


5.4 常见锁概念拓展
- 悲观锁 vs 乐观锁
- 悲观锁:我们使用的互斥锁就是典型的悲观锁,每次访问数据前都先加锁,认为数据一定会被修改,适用于写多读少的场景;
- 乐观锁:访问数据时不加锁,更新时判断数据是否被修改,通过版本号和 CAS 操作实现,适用于读多写少的场景,性能远高于悲观锁。
- CAS 操作:Compare-And-Swap,比较并交换,是乐观锁和无锁编程的核心。更新数据时,先判断当前内存值和之前读取的值是否相等,相等则更新,否则重试。现代 CPU 都提供了 CAS 原子指令。
- 自旋锁:申请锁失败时,线程不会被阻塞挂起,而是循环轮询尝试获取锁。适用于临界区执行时间极短的场景,避免线程切换的开销,缺点是长时间自旋会浪费 CPU 资源。
- 读写锁:针对读多写少场景优化的锁,读 - 读共享,写 - 写 / 读 - 写互斥。多个线程可以同时持有读锁,写锁同一时间只能被一个线程持有,大幅提升读多写少场景的并发度。


结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:本文从池化技术的核心思想出发,完整讲解了线程池的设计原理,手撕了工业级 C++ 线程池的完整实现,再到单例模式的进阶优化,最后深入拆解了线程安全、死锁、锁机制等底层核心问题,完整覆盖了 Linux C++ 高并发编程中线程池的全链路知识点。线程池是高并发后端开发的基石,它的本质是生产者消费者模型的工程化落地,核心思想是用预申请换响应速度,用统一管理换系统稳定性,用资源复用换系统开销。而线程池的底层,离不开互斥锁、条件变量这些同步互斥原语,更离不开对线程安全、死锁等问题的深刻理解。在实际的工业级开发中,线程池还会有更多进阶优化,比如动态调整线程数量的浮动线程池、任务优先级队列、线程异常处理、监控统计等功能,但核心的设计原理永远不会变。希望这篇文章能帮你彻底掌握线程池,解锁 Linux C++ 高并发编程的核心能力。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
