目录
[1. 为什么需要线程池](#1. 为什么需要线程池)
[2. 什么是线程池](#2. 什么是线程池)
[3. 应用场景](#3. 应用场景)
[4. 线程池种类](#4. 线程池种类)
[1. Thread 类](#1. Thread 类)
[2. Mutex 与 LockGuard](#2. Mutex 与 LockGuard)
[3. Condition 类](#3. Condition 类)
[4. 封装的价值](#4. 封装的价值)
[1. 为什么需要日志](#1. 为什么需要日志)
[2. 日志系统实现](#2. 日志系统实现)
[3. 代码架构分析](#3. 代码架构分析)
[1. 为什么需要单例](#1. 为什么需要单例)
[2. 单例模式的特点](#2. 单例模式的特点)
[1. 饿汉模式](#1. 饿汉模式)
[2. 懒汉模式](#2. 懒汉模式)
[3. 线程安全与双重 if 判定](#3. 线程安全与双重 if 判定)
[4. C++11 静态局部变量](#4. C++11 静态局部变量)
[1. 核心结构](#1. 核心结构)
[2. 工作流程](#2. 工作流程)
[3. 代码实现](#3. 代码实现)
一、线程池概念
本篇我们将进入多线程编程的实战领域。如果说 "生产者消费者模型" 是多线程协作的地基 ,那么线程池 、日志系统 与单例模式 的结合,则是构建高性能服务器程序的关键架构
我们将探讨如何通过预分配资源来充分发挥 CPU 性能,以及如何利用设计模式确保系统的稳健与优雅
1. 为什么需要线程池
在没有线程池的情况下,我们通常采用 "即时创建,即时销毁" 的模式。这种模式在处理海量短连接请求时,会暴露出三个核心缺陷:
-
创建/销毁开销大:创建线程涉及到内核栈的分配、TCB 的初始化、系统调用的开销。对于执行时间极短的任务,线程创建销毁的耗时甚至超过了任务本身的执行时间
-
响应延迟:如果等请求到了才开始创建线程,用户会感受到明显的处理延迟
-
资源溢出风险:如果不加限制地创建线程,海量请求会瞬间耗尽系统的 CPU 和内存资源,导致操作系统瘫痪(OOM 或 频繁的上下文切换)
2. 什么是线程池
线程池 是一种基于 "预分配" 思想的池化技术
-
池化核心 :在程序启动或初期,预先创建一定数量的线程,并让它们处于休眠等待状态
-
工作机制:当任务到达时,从池中挑选一个空闲线程去处理;任务执行完毕后,线程并不退出,而是回到池中等待下一个任务
-
关键组件:
-
任务队列:用于存放待处理的任务
-
线程集合:负责从队列中取出任务并执行的执行流
-
同步机制:通常使用互斥锁和条件变量来管理队列的并发访问
-

3. 应用场景
线程池最擅长处理 "任务量大、单个任务处理时间短" 的情况:
-
Web 服务器:如 Nginx、Apache 或自定义的高并发 HTTP Server,每个 HTTP 请求都是一个任务
-
数据库连接池:虽然是连接池,但其底层往往配合线程池来处理 SQL 的异步执行
-
计算密集型任务:如视频转码、图像处理,将大任务拆分成小块,交给线程池并行计算
-
异步任务处理:在主逻辑中不想被阻塞的操作(如发送邮件、写日志),丢进线程池慢慢跑
4. 线程池种类
根据业务需求的不同,线程池通常分为以下几种典型类型:
| 种类 | 特点 | 适用场景 |
|---|---|---|
| 固定大小线程池 | 核心线程数固定,任务多则排队 | 负载较稳定的长期任务,保护系统不超载 |
| 缓存线程池 | 根据需要创建新线程,空闲后自动回收 | 处理大量短生命周期的异步任务,弹性大 |
| 定时线程池 | 支持延迟执行或周期性重复执行任务 | 定时清理缓存、心跳检测、周期报表 |
| 单线程线程池 | 只有一个工作线程,保证任务按顺序执行 | 顺序敏感的日志写入、特定队列处理 |
线程池本质上是生产者消费者模型的一个应用案例。外部请求是生产者,线程池内的线程是消费者,而任务队列就是那个交易场所。它通过"空间换时间"和"集中式管理",极大地提升了系统的并发处理能力和稳定性
二、基础组件封装
在深入探讨线程池的具体实现之前,我们需要先准备好开发工具。直接使用原生 pthread 接口虽然功能全面,但在处理复杂业务逻辑时容易导致代码结构混乱,产生难以维护的 "意大利面条式代码"
通过封装,我们将底层 API 转化为符合 RAII 原则和面向对象思想的 C++ 类,这不仅是为了好看,更是为了通过编译器和作用域自动管理资源
1. Thread 类
封装线程类最核心的难题在于:pthread_create 要求的回调函数必须是 static 的(因为成员函数带有一个隐藏的 this 指针)
我们通过静态代理模式来解决这个问题
cpp
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
class Thread {
public:
using func_t = std::function<void()>; // 任务类型
Thread(func_t func, std::string name = "none")
: _func(func), _name(name), _tid(0), _is_running(false) {}
// 静态中间人:剥离 this 指针,适配 C 接口
static void* thread_routine(void* args) {
Thread* self = static_cast<Thread*>(args);
self->_func(); // 执行真正的成员任务
return nullptr;
}
void start() {
if (pthread_create(&_tid, nullptr, thread_routine, this) == 0) {
_is_running = true;
}
}
void join() {
if (_is_running) {
pthread_join(_tid, nullptr);
_is_running = false;
}
}
void detach() {
if (_is_running) {
pthread_detach(_tid);
_is_running = false;
}
}
private:
pthread_t _tid;
std::string _name;
bool _is_running;
func_t _func;
};
2. Mutex 与 LockGuard
Mutex 负责封装底层的锁,而 LockGuard 则利用局部变量的生命周期,确保即便代码运行出错,锁也能被安全释放
cpp
class Mutex {
public:
Mutex() { pthread_mutex_init(&_lock, nullptr); }
~Mutex() { pthread_mutex_destroy(&_lock); }
void lock() { pthread_mutex_lock(&_lock); }
void unlock() { pthread_mutex_unlock(&_lock); }
pthread_mutex_t* get_lock() { return &_lock; }
private:
pthread_mutex_t _lock;
};
// RAII 锁
class LockGuard {
public:
LockGuard(Mutex& mtx) : _mtx(mtx) { _mtx.lock(); }
~LockGuard() { _mtx.unlock(); }
private:
Mutex& _mtx;
};
3. Condition 类
条件变量用于管理线程状态:当任务队列为空时让线程挂起,当新任务到达时立即唤醒线程。我们对原生的 wait、signal(notify) 和 broadcast(notifyAll) 操作进行了语义化封装
cpp
class Condition {
public:
Condition() { pthread_cond_init(&_cond, nullptr); }
~Condition() { pthread_cond_destroy(&_cond); }
// wait 必须配合互斥锁使用
void wait(Mutex& mtx) {
pthread_cond_wait(&_cond, mtx.get_lock());
}
// 唤醒一个线程
void notify() {
pthread_cond_signal(&_cond);
}
// 唤醒所有线程
void notifyAll() {
pthread_cond_broadcast(&_cond);
}
private:
pthread_cond_t _cond;
};
4. 封装的价值
如果你直接在线程池代码里写 pthread_mutex_lock,万一在某个逻辑分支 return 了,你就得手动检查并写上 unlock。一旦代码量过千,几乎必然会出 Bug
有了这套组件后,我们的代码就可以这么写
cpp
{
LockGuard lock(_pool_mtx); // 自动加锁
while (_task_queue.empty()) {
_cond.wait(_pool_mtx); // 自动解、挂、锁
}
// 处理逻辑
} // 作用域结束,lock 析构自动解锁
这种 "无感同步" 的设计理念,正是工业级代码能够长期稳定运行的关键所在
三、日志系统
在多线程环境下,尤其是当我们构建了线程池后,普通的 printf 或 std::cout 就显得捉襟见肘了。当10 个线程同时向屏幕打印信息,字符会像乱码一样交织在一起
为了记录线程池的运行状态、排查生产环境的 Bug,我们需要一套硬核的日志系统
1. 为什么需要日志
在复杂的并发系统中,日志犹如程序员的 "第三只眼":
-
离线追踪:多线程 Bug(如死锁、竞态)往往转瞬即逝,无法通过 GDB 实时捕捉,只能通过查阅日志复盘
-
性能分析:记录任务进入和退出线程池的时间点,分析系统瓶颈
-
责任判定:在分布式或多模块协作中,通过日志确定是哪个环节出了问题
2. 日志系统实现
以下是我们基于策略模式 和RAII 思想实现的日志系统代码:
cpp
#pragma once
#include <iostream>
#include <string>
#include <filesystem>
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp" // 包含前面封装的 Mutex 类
// 1. 定义日志等级
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };
std::string Level2String(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "Debug";
case LogLevel::INFO: return "Info";
case LogLevel::WARNING: return "Warning";
case LogLevel::ERROR: return "Error";
case LogLevel::FATAL: return "Fatal";
default: return "Unknown";
}
}
// 获取当前格式化时间
std::string GetCurrentTime() {
time_t currtime = time(nullptr);
struct tm currtm;
localtime_r(&currtime, &currtm);
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return timebuffer;
}
// 2. 抽象刷新策略(基类)
class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 策略一:显示器刷新
class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const std::string &logmessage) override {
LockGuard lockguard(_lock); // 线程安全保证
std::cout << logmessage << std::endl;
}
private:
Mutex _lock;
};
// 策略二:文件刷新
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string &dir = "log",
const std::string filename = "test.log")
: _dir_path_name(dir), _filename(filename)
{
if (!std::filesystem::exists(_dir_path_name)) {
std::filesystem::create_directories(_dir_path_name);
}
}
void SyncLog(const std::string &logmessage) override
{
LockGuard lockguard(_lock);
std::string target = _dir_path_name + "/" + _filename;
std::ofstream out(target, std::ios::app);
if (out.is_open())
{
out << logmessage << "\n";
out.close();
}
}
private:
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
// 3. 日志管理器
class Logger {
public:
void EnableConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 内部类:负责流式构造日志消息
class LogMessage
{
public:
LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)
: _logger(logger)
{
std::stringstream ss;
ss << "[" << GetCurrentTime() << "] "
<< "[" << Level2String(level) << "] "
<< "[" << getpid() << "] " << "["
<< filename << "] " << "[" << line << "] - ";
_loginfo = ss.str();
}
template<typename T>
LogMessage& operator << (const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage() { // 析构时触发刷新逻辑
if(_logger._strategy) _logger._strategy->SyncLog(_loginfo);
}
private:
std::string _loginfo;
Logger &_logger;
};
LogMessage operator()(LogLevel level, std::string filename, int line) {
return LogMessage(level, filename, line, *this);
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
3. 代码架构分析
这段代码的精妙之处在于它不仅解决了打印问题,还构建了一个可扩展的框架
(1) 抽象刷新策略
通过 LogStrategy 虚基类,我们实现了 "日志生成" 与 "日志输出" 的彻底解耦
-
ConsoleLogStrategy 和 FileLogStrategy 分别处理不同的输出目标
-
这种设计模式使得系统非常容易扩展,比如未来想增加一个 NetworkLogStrategy 把日志发往远程服务器,只需继承基类并重写 SyncLog 即可,无需改动原有的业务逻辑
(2) 互斥
在每一个具体的 SyncLog 实现中,我们都使用了 LockGuard lockguard(_lock)
-
原子性:锁保证了同一时刻只有一个线程能操作显示器或文件
-
防篡改:如果没有锁,在高并发下,日志信息可能会出现半截 A 线程、半截 B 线程的情况
(3) 流式构造与 RAII 析构刷新
这是该代码最巧妙的设计点。我们利用了 C++ 的临时对象生命周期:
-
构造:当调用 LOG(INFO) << "..." 时,产生了一个 LogMessage 临时对象,构造函数自动填充时间、行号等元数据
-
传输:通过重载 operator<<,用户可以像使用 std::cout 一样拼接各种类型的数据
-
刷新 :当这一行代码执行结束,临时对象会被销毁(调用析构函数)
-
同步:析构函数内部调用了 SyncLog,将拼接好的完整字符串一次性刷新到目标设备
目前的实现属于同步日志,即:业务线程必须等日志写入磁盘/屏幕后才能继续。对于追求极致吞吐量的线程池来说,磁盘 I/O 可能拖慢效率
在后续的优化中,我们可以考虑让 Logger 内部持有一个阻塞队列,业务线程只管把日志丢进队列,由一个专门的 "日志持久化线程" 去负责慢慢写入磁盘,实现真正的非阻塞式日志记录
四、单例模式
在构建了基础组件和日志系统后,我们面临一个实际问题:谁来管理这些资源?
如果我们在程序的各个角落都创建一个 ThreadPool 实例,或者每个模块都维护一个独立的 Logger 写入同一个文件,必然会导致内存浪费、资源竞争以及逻辑上的混乱。为了确保全局逻辑的唯一性,我们需要引入单例模式
1. 为什么需要单例
单例模式的核心目的只有一个:确保一个类在整个程序的生命周期内,有且仅有一个实例,并提供一个全局访问点
结合我们的项目背景,单例的必要性体现在以下两点:
-
线程池唯一性:线程池本质上是一种消耗大量系统资源的重型组件。若在单个进程中创建多个线程池,不仅无法提升任务处理效率,反而会因过多的执行流导致 CPU 频繁进行上下文切换,造成严重的资源浪费
-
日志系统唯一性: 在上一节中,我们的日志策略涉及对文件的操作。如果存在多个 Logger 实例同时向同一个 test.log 文件写入,即便每个实例内部有锁,不同实例之间也无法感知彼此的锁定状态,这会导致文件指针冲突或日志条目交织
2. 单例模式的特点
单例模式的特点主要体现在以下三个维度:
(1) 全局唯一对象
这是单例模式最直观的特征
-
逻辑一致性 :在整个程序运行期间,该类只能存在一个实体
-
避免冲突:
-
对于日志系统,唯一性意味着只有一个对象在操作文件句柄,避免了多个实例同时写入导致的数据覆盖或乱序
-
对于线程池,唯一性意味着全程序共享同一组执行流,防止重复申请线程导致系统负载过载
-
(2) 全局访问点
单例模式提供了一个唯一访问入口
-
简化传参:如果不用单例,我们需要将 Logger 对象的引用从 main 函数开始,层层传递给每一个需要打日志的子函数
-
程序中的任何模块、任何线程,只要包含了头文件,就能通过统一的接口找到这个唯一的实例,而不需要关心这个实例是谁创建的、在哪创建的
(3) 生命周期统一
单例模式将资源的生命周期管理权从外部使用者移交给了类本身
-
状态可控:单例对象通常在第一次被使用时初始化,并一直存活到整个进程结束
-
职责明确:调用者只需要使用资源,不需要操心资源何时该被销毁。这种机制在管理像线程池这种需要伴随程序始终的资源时,显得尤为稳健
五、饿汉与懒汉
理清了特点后,我们就需要决定什么时候创建这个唯一的对象。根据创建时机的不同,通常将其分为饿汉与懒汉两种策略
1. 饿汉模式
核心思想: 就像一个"饿汉"看到饭就想吃一样,该模式在类加载时就完成了实例的创建
-
优点: 实现简单,天然是线程安全的(因为实例在主线程启动前、或在单线程环境下就已经初始化好了)
-
缺点: 如果这个实例很大且程序全程没用到,会造成内存浪费,也就是所谓的 "资源浪费"
代码实现:
cpp
class Singleton
{
public:
// 获取唯一示例的接口
static Singleton* Getinstance() { return &instance;}
public:
// 将构造函数私有, 方式外部 new
Singleton() {}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态成员变量,类内声明,类外初始化
static Singleton instance;
}
// 在类外直接初始化
Singleton Singleton::instance;
2. 懒汉模式
核心思想: 像个懒汉一样,不到万不得已(第一次调用 GetInstance 时)绝不干活,即延迟加载
-
优点: 节省内存,只有在真正需要时才会申请空间
-
缺点: 非线程安全。在多线程环境下,如果两个线程同时进入,可能会创建出两个不同的实例
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
// 只有第一次调用时才创建
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance;
};
// 初始化为空指针
Singleton* Singleton::instance = nullptr;
总结
要实现一个标准的单例模式,必须在类设计上满足以下四个条件:
-
私有化构造与析构函数:防止外部通过 new 或声明局部变量的方式随意创建对象
-
禁用拷贝构造与赋值运算符:使用 delete 关键字,杜绝通过拷贝或赋值产生副本的可能
-
静态私有实例指针:在类内部维护一个 static 修饰的自身指针,指向唯一实例
-
静态公有获取接口:提供一个 GetInstance() 静态成员函数,作为全局获取该实例的唯一途径
3. 线程安全与双重 if 判定
在上一节中我们提到,基础的懒汉模式在多线程下是不安全的。为了解决这个问题,我们最直观的想法就是加锁
1 简单的加锁改造
我们可以使用 mutex 对创建实例的过程进行保护:
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
// 每次调用 GetInstance 都会加锁
LockGuard lock(mtx);
if (instance == nullptr)
instance = new Singleton();
return instance;
}
private:
Singleton() {}
static Singleton* instance;
static Mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
为什么这种写法不好?
虽然上面的代码确实保证了线程安全,但它引入了一个致命的性能问题。单例模式在程序运行期间可能会被频繁调用,而实例只需要被创建一次。 在上述代码中,即使示例已经被创建了,后续的每一次 GetInstance() 调用依然会进行加锁、解锁操作。在多线程高并发环境下,这种无意义的锁竞争会严重拖慢程序性能
2 引入双重 if 判定
为了解决上述加锁带来的性能开销,我们引入了"双重 if 判定"模式。它的核心理念是:只在实例未创建时才加锁
cpp
class Singleton
{
public:
static Singleton* GetInstance()
{
// 第一重 if 判定:如果实例已经存在,直接返回,避免无谓的锁竞争
if (instance == nullptr)
{
LockGuard lock(mtx); // 加锁保护创建过程
// 当第一个排队的线程释放锁后,如果没有这层判定,后续线程依然会重新 new
if (instance == nullptr)
instance = new Singleton();
}
return instance;
}
private:
Singleton() {}
static Singleton* instance;
static Mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
深入理解两层 if 的作用:
-
外层 if:提高效率。99.9% 的情况下,实例已经被创建,直接跳过加锁环节,直接返回指针
-
内层 if:保证安全。假设线程 A 和 B 同时来到第一层 if,都发现是 nullptr。A 抢到了锁,进去 new 了一个对象,释放锁。此时 B 拿到锁进去,如果没有内层 if,B 就会再 new 一个对象,导致内存泄漏和逻辑错误
4. C++11 静态局部变量
虽然双重 if 判定解决了性能和安全问题,但代码依然显得有些臃肿。还有没有更优雅、更简单的方案呢?
答案是肯定的,那就是利用 C++11 的静态局部变量
核心原理
C++11 标准中明确规定:如果多个线程试图同时初始化同一个静态局部变量,那么初始化过程是线程安全的(编译器在底层自动帮我们加了锁,保证只初始化一次)
cpp
class Singleton {
public:
// 获取唯一实例的接口
static Singleton& GetInstance()
{
// 静态局部变量,生命周期与程序共存亡
// C++11 保证这里的初始化是线程安全的
static Singleton instance;
return instance;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
为什么这是最佳实践?
-
极其简洁: 不需要 mutex,不需要双重 if 检查,短短几行代码搞定
-
绝对安全: 语言标准层面的线程安全保证,不怕指令重排
-
延迟加载(懒汉式): 局部静态变量只有在程序第一次执行到该行代码时才会初始化,做到了按需加载
-
无内存泄漏: 返回的是对象的引用,而不是 new 出来的指针。程序结束时,系统会自动调用静态变量的析构函数,你不需要手动去考虑何时 delete 去释放内存
六、线程池实现
在理解了单例模式后,我们来看它的最佳实践对象之一:线程池 。线程池的核心意义在于复用线程,避免频繁创建和销毁线程带来的系统开销
1. 核心结构
一个工业级的线程池主要由两部分组成,形成典型的生产者-消费者模型:
-
任务队列: 这是一个临界资源,通常用 std::queue 存储待处理的任务(可以是函数对象、任务类等)
-
工作线程: 一组预先创建好的线程。它们不断地从任务队列中 "竞争" 任务并执行
2. 工作流程
线程池的运行逻辑如下:
-
初始化: 创建指定数量的线程,并启动它们,让它们进入等待状态
-
放入任务: 主线程(生产者)将任务丢进队列
-
唤醒线程: 每放一个任务,通过条件变量唤醒一个正在休眠的线程
-
执行任务: 被唤醒的线程从队列中取出任务,释放锁,然后执行任务逻辑
为什么需要条件变量?
如果没有条件变量,工作线程为了检查队列是否有任务,必须在一个 while 循环里不断地加锁、判断、解锁。这会极大地浪费 CPU 资源。 条件变量 让线程在队列为空时进入挂起状态,不占用 CPU;一旦有新任务再被唤醒
3. 代码实现
我们将使用之前封装好的组件来实现这个单例式线程池
cpp
#include <vector>
#include <queue>
#include <string>
#include <functional>
// 任务类型,通常是一个包装好的函数对象
using task_t = std::function<void()>;
class ThreadPool {
public:
// 获取单例实例的唯一接口
static ThreadPool* GetInstance()
{
// C++11 保证了静态局部变量初始化的线程安全性
static ThreadPool instance(5); // 默认开启 5 个工作线程
return &instance;
}
// 启动线程池
void start() {
for (auto &t : _threads) {
t->start();
}
LOG(LogLevel::INFO) << "threads started";
}
// 投放任务
void push_task(task_t task) {
{
LockGuard lock(_mtx);
_task_queue.push(task);
}
LOG(LogLevel::DEBUG) << "task pushed";
_cond.notify();
}
private:
// 构造函数私有化
ThreadPool(int num) : _num(num), _is_stop(false)
{
for (int i = 0; i < _num; ++i)
{
_threads.push_back(new Thread(
std::bind(&ThreadPool::thread_run, this),
"thread-" + std::to_string(i + 1)
));
}
}
// 禁用拷贝构造和赋值
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
~ThreadPool() {
LOG(LogLevel::INFO) << "~ThreadPool()";
_is_stop = true;
_cond.notifyAll(); // 唤醒所有线程让它们看到停止标志
for (auto &t : _threads) {
t->join();
LOG(LogLevel::DEBUG) << "Thread joined";
delete t;
}
}
// 内部工作逻辑
void thread_run()
{
pthread_t tid = pthread_self();
LOG(LogLevel::DEBUG) << "[" << tid << "] waiting.";
while (true)
{
task_t task;
{
LockGuard lock(_mtx);
// 等待条件:队列为空且没有停止信号就一直等
while (_task_queue.empty() && !_is_stop) {
_cond.wait(_mtx);
}
// 如果池子要停了且队列没任务了,线程直接退出
if (_is_stop && _task_queue.empty()) break;
// 否则,正常取任务
task = _task_queue.front();
_task_queue.pop();
}
// 拿到任务后,在锁之外执行,提高并发效率
if (task) task();
}
}
private:
int _num;
std::vector<Thread*> _threads;
std::queue<task_t> _task_queue;
Mutex _mtx;
Condition _cond;
bool _is_stop;
};
关键点解析:
-
**std::bind 的使用:**由于 Thread 构造函数接收的是 void() 类型,我们利用 std::bind 将类成员函数 thread_run 与 this 指针绑定,转换成 Thread 能够识别的格式
-
双重检查: 在 thread_run 中,我们使用 while(_task_queue.empty()) 而不是 if。这是 Linux 多线程编程的黄金法则,旨在应对虚假唤醒的情况
-
优雅退出: 在析构函数中,我们先设置 _is_stop = true,然后调用 _cond.notifyAll()
-
这确保了那些正在 wait 阻塞的线程会被立刻唤醒
-
唤醒后,它们会执行 if (_is_stop && _task_queue.empty()) break,从而安全地结束生命周期,避免了程序关闭时的死锁或僵尸线程
-
这里的静态局部变量单例会在程序生命周期结束时自动触发析构函数,从而带动 Thread 对象的销毁和 join,不需要手动管理内存释放,非常安全
使用示例:
cpp
int main() {
// 1. 配置日志策略:输出到控制台
logger.EnableConsoleLogStrategy();
// 2. 获取并启动线程池
ThreadPool* tp = ThreadPool::GetInstance();
tp->start();
// 3. 投放任务
for(int i = 1; i <= 3; ++i) {
tp->push_task([i](){
LOG(LogLevel::INFO) << "Executing task " << i;
sleep(1); // 模拟耗时操作
});
}
sleep(5); // 等待任务处理完
return 0;
}
总结
综上所述,从线程封装、到日志系统、单例模式以及线程池的实现,我们已经开始迈向 "组织线程与任务" 的工程化阶段
其中,线程池通过 "任务队列 + 工作线程" 的模式,大幅降低了频繁创建线程的开销;而线程安全单例、线程安全日志等设计,则进一步体现了并发环境下资源管理的重要性
与此同时,无论是双重判定的懒汉单例,还是条件变量中的等待与唤醒,本质上都指向了同一个核心问题:
多个执行流同时运行时,程序行为将不再完全可控
也正因如此,并发编程中除了 "同步"之外,还会伴随着死锁、重入、可见性等更加隐蔽的问题
在下一篇中,我们将继续深入并发编程的底层细节,进一步分析死锁、可重入问题等经典并发陷阱
