
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- 前言
- [1 ~> 开始:准备阶段](#1 ~> 开始:准备阶段)
-
- [1.1 为什么需要线程池?](#1.1 为什么需要线程池?)
-
- [1.1.1 "点菜"与"预制菜"的比喻](#1.1.1 “点菜”与“预制菜”的比喻)
- [1.1.2 线程池(Thread Pool)是一种基于"池化"思想的资源管理机制](#1.1.2 线程池(Thread Pool)是一种基于“池化”思想的资源管理机制)
- [1.2 核心模型:线程池其实就是一个"生产者消费者模型"](#1.2 核心模型:线程池其实就是一个“生产者消费者模型”)
- [1.3 实验验证](#1.3 实验验证)
-
- [1.3.1 线程封装与启动](#1.3.1 线程封装与启动)
- [1.3.2 任务调度例程(Routine)](#1.3.2 任务调度例程(Routine))
- [1.4 关键特性提取](#1.4 关键特性提取)
- [1.5 总结与推演](#1.5 总结与推演)
- [2 ~> 线程池(Thread Pool)的初步实现](#2 ~> 线程池(Thread Pool)的初步实现)
-
- [2.1 线程池的内部结构](#2.1 线程池的内部结构)
- [2.2 线程池核心实现(ThreadPool.hpp)](#2.2 线程池核心实现(ThreadPool.hpp))
- [3 ~> 线程池的最佳实践](#3 ~> 线程池的最佳实践)
-
- [3.1 第一步:构建基础组件](#3.1 第一步:构建基础组件)
-
- [3.1.1 任务定义 (Task.hpp)](#3.1.1 任务定义 (Task.hpp))
- [3.1.2 线程封装 (Thread.hpp)](#3.1.2 线程封装 (Thread.hpp))
- [3.2 第二步:手撕核心 ThreadPool 类](#3.2 第二步:手撕核心 ThreadPool 类)
-
- [3.2.1 解决 this 指针的"优雅姿势"](#3.2.1 解决 this 指针的“优雅姿势”)
- [3.2.2 核心实现代码 (ThreadPool.hpp)](#3.2.2 核心实现代码 (ThreadPool.hpp))
- [3.3 第三步:温和地"劝退"------线程安全退出](#3.3 第三步:温和地“劝退”——线程安全退出)
- [3.4 第四步:升华------单例模式(懒汉版 + 双重检查锁定)](#3.4 第四步:升华——单例模式(懒汉版 + 双重检查锁定))
-
- [3.4.1 为什么用双重检查锁定 (Double-Check Locking)?](#3.4.1 为什么用双重检查锁定 (Double-Check Locking)?)
- [4 ~> 总结与思考](#4 ~> 总结与思考)
-
- [4.1 总结](#4.1 总结)
- [4.2 思考](#4.2 思考)
- 结尾
前言
在高性能服务器开发中,"线程池"是一个绕不开的话题。你是否好奇过------为什么像 Nginx、Redis( 6.0 后)或者 Java 的 ExecutorService 底层都要维护一个池子?今天,我们就脱离复杂的库函数,从底层原理出发,手写一个高性能、生产级的 C++ 线程池。
1 ~> 开始:准备阶段
1.1 为什么需要线程池?
1.1.1 "点菜"与"预制菜"的比喻
想象一下你去一家餐馆吃饭:
-
传统方案:你点一份"鱼香肉丝",老板现去招一个厨师,厨师洗手、穿衣服、炒菜,炒完菜后老板直接把厨师辞退了。下次有人点菜,重复这个过程。
- 代价:招人和辞退人的开销(创建和销毁线程的系统调用)远比炒菜本身(执行任务)大得多。
-
池化方案(线程池):老板提前招好 5 个厨师在后厨待命。你一点菜,后厨主管(线程池管理器)立马把单子丢给闲着的厨师。厨师炒完这顿,原地待命等下一单。
- 优势:预制化生产,响应速度极快,且避免了频繁申请 / 释放资源的成本。 这种"池化技术"在内存池、连接池中也广泛应用。
1.1.2 线程池(Thread Pool)是一种基于"池化"思想的资源管理机制
在多线程高并发架构中,频繁创建与销毁线程会带来显著的 OS 调度开销和资源浪费。线程池(Thread Pool)是一种基于"池化"思想的资源管理机制。其核心逻辑是预先在进程空间内创建一组待命执行流,通过维护一个任务队列(Task Queue),实现任务的投放(Enqueue)与处理(Pop)在时空上的解耦。
这种架构不仅能快速响应外部请求("先种菜再点菜"的预分配策略),还能通过限制并发线程总量,防止系统因负载过高而崩溃,是构建健壮网络服务器的关键组件。
1.2 核心模型:线程池其实就是一个"生产者消费者模型"
线程池的本质非常纯粹:一个典型的多生产者多消费者模型(CP Model)。
-
生产者:主线程或其他业务线程,负责不断地将"任务"(Task)推送到队列中。
-
队列 :缓冲地带,通常是
std::queue。 -
消费者 :线程池预先创建好的
n个线程,它们竞争式地从队列里取任务并执行。
线程池的运行机制可抽象为生产者-消费者模型,其内部组件逻辑如下:
(1)任务容器 :通常采用阻塞队列或环形队列存储待处理的任务函数(或函数对象)。
(2)线程组 :一组通过 pthread_create 预先创建的 worker 线程。
(3)同步互斥链路:利用互斥锁(Mutex)确保多个 worker 线程在争抢任务时的原子性,利用条件变量(Condition Variable)实现执行流的挂起与唤醒。

1.3 实验验证
一个典型的 C++ 线程池实现需结合 pthread 封装与单例模式。
1.3.1 线程封装与启动
线程池初始化时,需通过循环创建指定数量的执行流,并将每个线程绑定到统一的调度入口 Routine。
cpp
// 线程启动逻辑
void Start() {
for(int i = 0; i < _thread_num; i++) {
_threads.emplace_back(std::bind(&ThreadPool::Routine, this));
LOG(INFO, "Thread created successfully");
}
}
1.3.2 任务调度例程(Routine)
Worker 线程在启动后进入无限循环,通过条件变量判断队列状态。若队列为空,线程挂起;若有任务,则通过互斥锁竞争获取任务执行权。
cpp
void Routine() {
while(true) {
T task;
{
LockGuard lock(&_mutex);
while(_task_queue.empty()) {
_cond.Wait(); // 队列为空,线程挂起
}
task = _task_queue.pop(); // 竞争获取任务
}
task.Run(); // 在锁外执行,确保高并发处理性能
}
}
1.4 关键特性提取
(1)策略模式与日志组件:文档强调将多线程组件化。通过引入日志等级(INFO, DEBUG, ERROR)和时间戳,实现对线程池运行轨迹的可观测性,这是工业级开发区别于 Demo 开发的分水岭。
(2)双缓冲队列(交换队列)优化:在进阶架构中,为减少生产者与消费者对单一队列的锁竞争,可引入活跃队列(Active)与过期队列(Expired)。当 Active 为空时,仅需交换两个队列的指针,实现 O(1) 级别的调度切换,极大降低了无锁化趋势下的同步成本。
(3)预分配优势:规避了在高峰期创建线程的内存申请、内核数据结构初始化等高耗时操作,将业务响应延迟降低至微秒级。
1.5 总结与推演
线程池的设计本质是对系统资源分配权的收回。通过封装 pthread 接口与同步原语,开发者构建出了一个能够自我管理的逻辑执行层。
从系统级视角看,线程池与后续可能引入的"协程调度"或"双缓冲队列"一脉相承,其核心进化方向始终是:降低临界区的竞争粒度、减少内核态与用户态的上下文切换频率。对于未来复习而言,掌握线程池不仅是掌握一种设计模式,更是理解 Linux 环境下多线程协作、同步安全及系统性能调优的基石。
2 ~> 线程池(Thread Pool)的初步实现
我们已经知道,频繁地创建和销毁线程会带来巨大的系统开销。线程池通过预先创建一批线程并让它们处于待命状态,来解决这个问题。

2.1 线程池的内部结构
任务队列:存放待处理的任务。
线程组:不断从任务队列里拿任务执行。
同步机制:
-
_mutex:保护任务队列的原子操作。 -
_cond:如果队列为空,线程进入休眠;如果来了新任务,唤醒线程。
2.2 线程池核心实现(ThreadPool.hpp)
如何批量管理线程。
cpp
#include <vector>
#include <queue>
#include <functional>
#include <pthread.h>
class ThreadPool
{
public:
ThreadPool(int num = 5) : _thread_num(num)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond, NULL);
}
void Start()
{
for (int i = 0; i < _thread_num; i++)
{
pthread_t tid;
// 注意:在类成员函数中创建线程,需传入 static 函数或包装器
pthread_create(&tid, NULL, Routine, this);
_threads.push_back(tid);
}
}
void Push(std::function<void()> task)
{
pthread_mutex_lock(&_mutex);
_task_queue.push(task);
pthread_cond_signal(&_cond); // 唤醒一个线程
pthread_mutex_unlock(&_mutex);
}
static void* Routine(void* arg)
{
ThreadPool* tp = (ThreadPool*)arg;
while (true)
{
pthread_mutex_lock(&tp->_mutex);
while (tp->_task_queue.empty())
{
pthread_cond_wait(&tp->_cond, &tp->_mutex);
}
auto task = tp->_task_queue.front();
tp->_task_queue.pop();
pthread_mutex_unlock(&tp->_mutex);
task(); // 执行任务
}
return NULL;
}
private:
int _thread_num;
std::vector<pthread_t> _threads;
std::queue<std::function<void()>> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
3 ~> 线程池的最佳实践
3.1 第一步:构建基础组件
在写核心池之前,我们需要两个好帮手:互斥锁封装和任务类。
3.1.1 任务定义 (Task.hpp)
我们要处理的任务应该是通用的。在 C++11 之后,std::function 是最佳选择。
cpp
#pragma once
#include <iostream>
#include <functional>
// 任务类:支持任何无参无返回值的函数对象
class Task {
public:
using func_t = std::function<void()>;
Task() = default;
Task(func_t f) : _cb(f) {}
void operator()() {
if (_cb) _cb();
}
private:
func_t _cb; // 回调函数
};
3.1.2 线程封装 (Thread.hpp)
为了方便管理,我们对原生的 pthread 或 std::thread 进行简单封装。
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
class Thread {
public:
using func_t = std::function<void(const std::string&)>;
Thread(const std::string& name, func_t func)
: _name(name), _func(func) {}
static void* Routine(void* args) {
Thread* t = static_cast<Thread*>(args);
t->_func(t->_name);
return nullptr;
}
void Start() {
pthread_create(&_tid, nullptr, Routine, this);
}
void Join() {
pthread_join(_tid, nullptr);
}
private:
pthread_t _tid;
std::string _name;
func_t _func;
};
3.2 第二步:手撕核心 ThreadPool 类
这是整篇文章的重头戏。我们需要解决两个核心矛盾:
1、this 指针问题 :pthread_create 要求回调是静态函数,但静态函数没法直接访问类内的非静态成员。
2、锁的粒度:任务执行绝不能在临界区内!
3.2.1 解决 this 指针的"优雅姿势"
在 ThreadPool 内部启动线程时,我们使用 Lambda 表达式 捕获 this。这样既能满足 Thread 类的回调接口,又能让执行逻辑回到类成员函数中。
3.2.2 核心实现代码 (ThreadPool.hpp)
cpp
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include "Thread.hpp"
#include "Task.hpp"
template <typename T>
class ThreadPool {
public:
ThreadPool(int num = 5) : _thread_num(num), _is_running(false), _sleeper_cnt(0) {}
// 禁止拷贝和赋值(为单例做准备)
ThreadPool(const ThreadPool<T>&) = delete;
void operator=(const ThreadPool<T>&) = delete;
void Start() {
_is_running = true;
for (int i = 0; i < _thread_num; ++i) {
std::string name = "thread-" + std::to_string(i + 1);
// Lambda 捕获 this,解决静态方法访问成员的问题
_threads.emplace_back(name, [this](const std::string& name) {
this->ThreadRoutine(name);
});
_threads.back().Start();
}
}
// 生产者:向队列投喂任务
void Enqueue(const T& task) {
std::unique_lock<std::mutex> lock(_mtx);
_task_queue.push(task);
// 优化:如果有线程在睡觉,才唤醒
if (_sleeper_cnt > 0) {
_cond.notify_one();
}
}
// 消费者逻辑
void ThreadRoutine(const std::string& name) {
while (true) {
T task;
{
std::unique_lock<std::mutex> lock(_mtx);
// 退出条件:队列为空 且 线程池停止运行
while (_task_queue.empty() && _is_running) {
_sleeper_cnt++;
_cond.wait(lock);
_sleeper_cnt--;
}
// 如果池子停了且任务清空了,正式下班
if (_task_queue.empty() && !_is_running) {
break;
}
task = _task_queue.front();
_task_queue.pop();
}
// 关键:解锁后再执行任务!洗碗的时候不能占着水龙头不让别人排队
task();
}
}
~ThreadPool() { /* 调用 Stop 和 Join */ }
private:
std::vector<Thread> _threads;
std::queue<T> _task_queue;
std::mutex _mtx;
std::condition_variable _cond;
int _thread_num;
bool _is_running;
int _sleeper_cnt; // 记录正在休眠的线程数
};
3.3 第三步:温和地"劝退"------线程安全退出
很多初学者会直接用 pthread_cancel 强杀线程,这会导致内存泄漏或死锁(锁没释放)。
温和退出的逻辑:
1、设置 _is_running = false,断绝后续任务入队的可能。
2、调用 notify_all()。为什么要全叫醒?因为有些线程可能死死睡在 wait 上,不叫醒它们,它们永远没机会看到 _is_running 变了。
3、坚持到底 :线程在退出前,必须检查 _task_queue.empty()。即便老板要关门,后厨也得把剩下的菜炒完再走。
3.4 第四步:升华------单例模式(懒汉版 + 双重检查锁定)
在整个程序中,线程池通常只需要一个实例。为了节省资源,我们要把它设计成单例模式。
3.4.1 为什么用双重检查锁定 (Double-Check Locking)?
普通懒汉:每次获取实例都加锁,高并发下性能直接拉胯。
双重检查:
-
1、先看指针空不空,不空直接返回(不加锁,快!)。
-
2、空了才加锁,加锁后再看一眼空不空(防止两个线程同时过了第一道门)。
cpp
template <typename T>
class ThreadPool {
public:
static ThreadPool<T>* GetInstance() {
if (_instance == nullptr) { // 第一重检查
std::lock_guard<std::mutex> lock(_singleton_mtx);
if (_instance == nullptr) { // 第二重检查
_instance = new ThreadPool<T>();
}
}
return _instance;
}
// ... 其他代码 ...
private:
static ThreadPool<T>* _instance;
static std::mutex _singleton_mtx;
// 构造函数私有化
ThreadPool(int num = 5) : ... { }
};
// 静态成员初始化
template <typename T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;
template <typename T>
std::mutex ThreadPool<T>::_singleton_mtx;
4 ~> 总结与思考
4.1 总结
通过这篇"手撕"之旅,我们不仅实现了一个线程池,还串联了多个知识点:
-
池化思想:资源预分配,空间换时间。
-
CP 模型:通过互斥锁和条件变量协调生产与消费。
-
Lambda 的妙用:完美解决了 C 风格回调函数与 C++ 类对象的爱恨情仇。
-
单例模式:用双重检查锁定保障了线程安全与性能的平衡。
4.2 思考
1、如果任务执行时间极短,但任务量巨大,我们的线程池会有什么瓶颈?(提示:锁竞争)
2、目前的 _sleeper_cnt 优化主要是为了减少无效的 notify,你觉得在什么场景下它的作用最明显?
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux线程】Linux系统多线程(八):<策略模式>日志系统的封装实现
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
