本文要聊的,是线程池。
先从核心概念入手,把它的典型应用场景和选型分类铺开,让你对线程池有个整体的判断框架。然后往深里挖,把饿汉式和线程安全懒汉式这两种单例模式的实现原理逐一拆开,看清它们各自的门道。最后落到代码上,结合单例模式,给出一个完整可用的线程池实现。
目录
[1.1 线程池的基本概念与核心价值](#1.1 线程池的基本概念与核心价值)
[1.2 哪些场景适合使用线程池](#1.2 哪些场景适合使用线程池)
[1.2.1 高并发下的短任务处理](#1.2.1 高并发下的短任务处理)
[1.2.2 对响应速度要求较高的应用](#1.2.2 对响应速度要求较高的应用)
[1.2.3 面对突发流量时的线程资源控制](#1.2.3 面对突发流量时的线程资源控制)
[1.3 常见线程池类型与基本选型思路](#1.3 常见线程池类型与基本选型思路)
[2.1 单例模式是什么:从生活场景理解](#2.1 单例模式是什么:从生活场景理解)
[2.2 单例模式的实现与逐步演进](#2.2 单例模式的实现与逐步演进)
[2.2.1 饿汉模式:程序启动时完成实例创建](#2.2.1 饿汉模式:程序启动时完成实例创建)
[2.2.2 懒汉模式:按需创建实例及其隐患](#2.2.2 懒汉模式:按需创建实例及其隐患)
[2.2.3 加锁实现线程安全的懒汉模式](#2.2.3 加锁实现线程安全的懒汉模式)
[3.1 基础组件:线程池运行所需的基础设施](#3.1 基础组件:线程池运行所需的基础设施)
[3.1.1 Cond.hpp:条件变量封装](#3.1.1 Cond.hpp:条件变量封装)
[3.1.2 Mutex.hpp:互斥锁封装](#3.1.2 Mutex.hpp:互斥锁封装)
[3.1.3 Log.hpp:日志功能封装](#3.1.3 Log.hpp:日志功能封装)
[3.1.4 Task.hpp:任务对象定义](#3.1.4 Task.hpp:任务对象定义)
[3.2 核心组件:线程与线程池](#3.2 核心组件:线程与线程池)
[3.2.1 Thread.hpp:线程封装](#3.2.1 Thread.hpp:线程封装)
[3.2.2 ThreadPool.hpp:线程池核心实现](#3.2.2 ThreadPool.hpp:线程池核心实现)
[3.3 从编译到运行:构建与测试](#3.3 从编译到运行:构建与测试)
[3.3.1 Makefile:项目编译规则](#3.3.1 Makefile:项目编译规则)
[3.3.2 Main.cpp:线程池测试入口](#3.3.2 Main.cpp:线程池测试入口)
一、认识线程池:为什么需要线程池
1.1 线程池的基本概念与核心价值
线程池,说白了就是一种线程使用模式。多线程编程里,如果任由线程无节制地创建,数量一多,上下文切换和调度开销就会大得惊人,CPU的缓存局部性被破坏,系统整体性能跟着往下掉。
线程池的做法,是在内部预先养着一批线程,让它们处于等待状态,随时准备接住由管理者派发下来的并发任务。这套机制带来的核心收益,主要有两点:
-
资源消耗和开销极低:处理短任务时,不必反复创建、销毁线程,省去了那笔巨大的开销。
-
资源利用高效,还能防过载:既让系统内核和硬件资源得到充分利用,又能有效防止线程被过度调度。
系统里到底能同时跑多少线程,不是拍脑袋定的。它通常受制于硬件和系统资源的瓶颈,可用的并发处理器数量、处理器内核数、内存容量,还有网络socket句柄数量,都是天花板。
1.2 哪些场景适合使用线程池
线程池这东西,特性摆在那儿,现代服务端和高并发系统里,到处都能看到它的身影。
1.2.1 高并发下的短任务处理
任务量巨大,但单个任务跑起来没一会儿就结束,这种场景,线程池简直是为它量身定做的。最典型的例子,就是热门Web服务器扛住海量用户的点击请求。
反过来,如果是那种一跑就很久的长任务,比如Telnet连接请求,情况就不一样了。Telnet会话持续的时间,远远超过线程本身创建的开销,这时候再用线程池,性能提升就微乎其微,没什么意思。
1.2.2 对响应速度要求较高的应用
有些系统,对性能的要求苛刻到近乎偏执,客户端的请求必须秒回,比如实时交易系统、高频 RPC 服务。线程池在这里的价值,就是提前把工作线程备好。任务一到,立刻就能上手,响应近乎"零等待"。临时抱佛脚地创建线程?根本来不及。
1.2.3 面对突发流量时的线程资源控制
系统时不时会撞上突发流量,一瞬间,大量请求涌进来。没有线程池的时候,系统会慌不择路地疯狂创建线程。理论上,大部分操作系统支持的线程上限确实不低,但短时间内的爆发式创建,极容易把内存逼到极限,一不小心就引发严重错误。
线程池往那一挡,事情就稳了:该接的流量照接,线程总量却始终被摁在安全线以内。扛得住冲击,也守得住底线。
1.3 常见线程池类型与基本选型思路
实际工程里落地线程池,常见的主要是两种模式。
固定数量线程池:预先创建好固定数量的工作线程。这些线程一出生就进入循环,不断从任务队列里捞任务对象。捞到一个,立刻执行它身上的接口方法。线程数量定死,不增不减。
浮动线程池:线程数量不写死,而是根据系统当前的实际负载,动态地增减。核心调度逻辑跟固定线程池大差不差,区别只在于"要不要多开几个线程、要不要裁掉几个闲人"这一层。
本系列的设计与实现里,我们选的是固定个数的线程池。逻辑更清晰,落地也更好把控,拿来把线程池的原理吃透,正合适。

二、线程安全的单例模式:为什么线程池需要它
2.1 单例模式是什么:从生活场景理解
有些类,在整个系统里只应该有一个对象,这种设计模式,就叫单例模式。就像现实里,一个男人只能有一个媳妇,天然带着唯一性。
服务器开发里,这事儿特别常见。程序启动时,往往要把海量数据,上百G的配置、缓存,一把加载进内存。这时候要是反复创建对象,内存分分钟被撑爆,数据还容易各说各话,对不上号。所以,像这种需要统一管理的数据,特别适合交给单例模式的类来管。
单例模式有两种常见写法,饿汉方式 和懒汉方式。为了把它们讲得直观,拿生活中的"洗碗"打个比方:
饿汉方式:刚吃完饭,立刻把碗洗干净。好处是下一顿要吃的时候,随手就能拿起干净的碗用,加载迅速,随用随有。
懒汉方式:吃完饭先把碗搁着,不急着洗。直到下一顿真要用了,才临时抱佛脚去洗。
懒汉方式最核心的思想,就是延迟加载。服务端启动阶段,如果暂时用不上这些资源,那就先不初始化。启动速度,就这么被大幅优化出来了。
2.2 单例模式的实现与逐步演进
思路其实很简单:只要通过Singleton这个包装类去碰目标对象T,就能保证在一个进程里,T的实例有且只有一个。
2.2.1 饿汉模式:程序启动时完成实例创建
饿汉方式,主打一个"急脾气",类加载或者程序启动的时候,静态实例就已经初始化完毕,什么都不等。
cpp
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
data是静态成员,程序一启动它就位。谁来要实例,直接把地址甩过去。不用判断、不用加锁,省心。
2.2.2 懒汉模式:按需创建实例及其隐患
懒汉方式正好相反,不急着造,等第一次有人来要了,才动手创建:
cpp
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
看着挺聪明,实则埋着一颗雷:多线程安全。
设想多个线程同时第一次调用GetInstance()。它们齐刷刷跑到if (inst == NULL)跟前,一看,都是空的,于是个个都觉得"该我出手了"。结果,你new 一个,我new 一个,他再new 一个,好几份T对象的实例,就这么凭空冒了出来。单例约束,碎了一地。
当然,这颗雷只在"首次创建"那一瞬间引爆。等实例一旦造好,后面再多的并发调用,也只是读读指针,不会出问题。可问题恰恰就在那一瞬间,越是高并发,越容易撞上。
2.2.3 加锁实现线程安全的懒汉模式
想让懒汉模式在多线程环境下安全落地,光靠一个if显然不够。得请出互斥锁,再配上经典的**双重检查锁(Double-Checked Locking)**机制:
cpp
// 懒汉模式,线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要 volatile 关键字,防止指令重排与编译器优化
static std::mutex lock;
public:
static T* GetInstance() {
// 外层判定:先看一眼,实例是不是已经造好了
if (inst == NULL) {
lock.lock();
// 内层判定:加锁之后,再确认一次,防止多个线程同时闯入
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};
两层判断,各司其职,配合得天衣无缝。
外层判断,负责"偷懒"。实例一旦创建完成,后续每一次调用都直接从这里返回,根本不用去碰锁。锁冲突的概率被压到最低,性能自然上来了。
内层判断,负责"把关"。首次并发调用的那一刻,多个线程可能同时挤进外层判断。但锁一上,就只剩一个幸运儿能往下走。它再确认一次inst == NULL,然后执行new。等它释放锁,后面排队的线程冲进来,一看inst已经有值了,乖乖退出去,各回各家。new操作,有且仅有一次。
volatile关键字,则是给编译器提个醒:别自作聪明。它能防止编译器过度优化,也能挡住指令重排。否则,inst 指针有可能在对象还没完全构造好之前就被暴露出去,别的线程一读,拿到的就是一个半成品,后患无穷。
三、用单例模式实现一个线程池
3.1 基础组件:线程池运行所需的基础设施
线程池不是凭空冒出来的,它得靠几个基础件搭起来:互斥锁、条件变量、日志,还有任务定义。先把这几块砖备好,后面砌墙才顺手。
3.1.1 Cond.hpp:条件变量封装
条件变量的薄封装,把pthread_cond_t的初始化、等待、唤醒、销毁,包成好用的方法。
cpp
#ifndef __CONDMODULE__
#define __CONDMODULE__
#include <pthread.h>
#include "Mutex.hpp"
namespace ConModule
{
class Cond
{
public:
Cond()
{
int n = pthread_cond_init(&_cond, nullptr);
(void)n;
}
void Wait(MutexModule::Mutex &mutex)
{
int n = pthread_cond_wait(&_cond, mutex.Get());
(void)n;
}
void Signal()
{
int n = pthread_cond_signal(&_cond);
(void)n;
}
void Broadcast()
{
int n = pthread_cond_broadcast(&_cond);
(void)n;
}
~Cond()
{
int n = pthread_cond_destroy(&_cond);
(void)n;
}
private:
pthread_cond_t _cond;
};
}
#endif
Wait接收一个Mutex引用,通过mutex.Get()拿到原始的pthread_mutex_t指针,再交给pthread_cond_wait。条件变量的等待,天生就得跟互斥锁绑在一起,这层封装把这层关系顺理成章地接住了。
3.1.2 Mutex.hpp:互斥锁封装
互斥锁和RAII守卫的组合,一个管锁本体,一个管自动加解锁。
cpp
#ifndef __MUTEXMODULE__
#define __MUTEXMODULE__
#include <pthread.h>
#include <filesystem>
#include <iostream>
#include <ctime>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <fstream>
#include <sstream>
#include <memory>
#include <cstdio>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
int n = pthread_mutex_init(&_mutex, nullptr);
(void)n;
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
(void)n;
}
void UnLock()
{
int n = pthread_mutex_unlock(&_mutex);
(void)n;
}
~Mutex()
{
int n = pthread_mutex_destroy(&_mutex);
(void)n;
}
pthread_mutex_t* Get()
{
return &_mutex;
}
private:
pthread_mutex_t _mutex;
};
class MutexGuard
{
public:
MutexGuard(Mutex& mutex)
: _mutex(mutex)
{
_mutex.Lock();
}
~MutexGuard()
{
_mutex.UnLock();
}
private:
Mutex& _mutex;
};
}
#endif
Mutex把pthread_mutex_t包起来,Get()方法把原始指针暴露出去,方便条件变量那边调用。MutexGuard是RAII守卫,构造即加锁、析构即解锁,这次传的是引用,不会再把锁拷成两份了。
3.1.3 Log.hpp:日志功能封装
日志模块,策略模式撑骨架,RAII消息管输出。
cpp
#ifndef __LOG_MODULE__
#define __LOG_MODULE__
#include "Mutex.hpp"
namespace LogModule
{
const std::string CAGE = "\n";
// 刷盘策略抽象基类
class LogFlashStrategy
{
public:
~LogFlashStrategy() = default;
virtual void SyncLog(const std::string& message) = 0;
};
// 控制台输出策略
class ConsoleLogStrategy : public LogFlashStrategy
{
public:
void SyncLog(const std::string& logmessage) override
{
MutexModule::MutexGuard _MutexGuard(_mutex);
std::cout << logmessage << CAGE;
}
private:
MutexModule::Mutex _mutex;
};
const std::string default_filename = "log.txt";
const std::string default_filepath = "./log";
// 文件输出策略
class FileLogStrategy : public LogFlashStrategy
{
public:
FileLogStrategy(const std::string file = default_filename,
const std::string path = default_filepath)
: _filename(file), _filepath(path)
{
MutexModule::MutexGuard _MutexGuard(_mutex);
if (std::filesystem::exists(_filepath))
return;
try
{
std::filesystem::create_directories(_filepath);
}
catch (const std::filesystem::filesystem_error& _exception)
{
std::cout << _exception.what() << CAGE;
}
}
void SyncLog(const std::string& logmessage) override
{
MutexModule::MutexGuard _MutexGuard(_mutex);
std::string pathfile = _filepath +
(_filepath.back() == '/' ? "" : "/") + _filename;
std::ofstream out(pathfile, std::ios::app);
if (!out.is_open())
return;
out << logmessage << CAGE;
out.close();
}
private:
MutexModule::Mutex _mutex;
std::string _filename;
std::string _filepath;
};
// 日志等级
enum class LEVEL
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 获取当前时间字符串
std::string GetCurrentTime()
{
time_t cur_time = time(nullptr);
struct tm result;
localtime_r(&cur_time, &result);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
result.tm_year + 1900,
result.tm_mon + 1,
result.tm_mday,
result.tm_hour,
result.tm_min,
result.tm_sec);
return timebuffer;
}
// 等级转字符串
std::string LevelToString(LEVEL level)
{
switch (level)
{
case LEVEL::DEBUG: return "DEBUG";
case LEVEL::INFO: return "INFO";
case LEVEL::WARNING: return "WARNING";
case LEVEL::ERROR: return "ERROR";
case LEVEL::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
class Logger
{
public:
Logger()
{
EnableConsolLogStrategy();
}
void EnableConsolLogStrategy()
{
_current_flash_tsrategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_current_flash_tsrategy = std::make_unique<FileLogStrategy>();
}
// 一条日志消息,RAII 风格
class LogMessage
{
public:
LogMessage(LEVEL& level, std::string& file, size_t line, LogModule::Logger& logger)
: _current_time(GetCurrentTime()),
_level(level),
_pid(getpid()),
_src_file(file),
_line(line),
_logger(logger)
{
std::stringstream ret;
ret << "[" << _current_time << "]"
<< "[" << LevelToString(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _src_file << "]"
<< "[" << _line << "] -";
_hole_message = ret.str();
}
template<class T>
LogMessage& operator<<(const T& info)
{
std::stringstream ss;
ss << info;
_hole_message += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._current_flash_tsrategy)
_logger._current_flash_tsrategy->SyncLog(_hole_message);
}
private:
std::string _current_time;
LEVEL _level;
pid_t _pid;
std::string _src_file;
size_t _line;
std::string _hole_message;
LogModule::Logger& _logger;
};
LogMessage operator()(LEVEL level, std::string filename, size_t line)
{
return LogMessage(level, filename, line, *this);
}
private:
std::unique_ptr<LogFlashStrategy> _current_flash_tsrategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.EnableConsolLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.EnableFileLogStrategy()
}
#endif
3.1.4 Task.hpp:任务对象定义
任务定义,简单直白,就一个函数类型别名和一个示例任务。
cpp
#ifndef __TASK__
#define __TASK__
#include <functional>
#include <iostream>
using task_t = std::function<void(void)>;
void DownLoadTask(void)
{
std::cout << "这是一个下载任务" << std::endl;
}
#endif
task_t就是std::function<void(void)>,任务被抽象成可调用对象。任何签名匹配的函数、lambda、仿函数,都能往这个框里装。线程池接到的任务,就是这一份份等待被执行的 task_t。
四个基础组件到这就备齐了:Cond管等待与唤醒,Mutex管互斥与RAII,Log管日志输出,Task管任务抽象。下一步,就是拿它们拼出线程池的本体。
3.2 核心组件:线程与线程池
基础件备齐了,现在轮到主角登场。Thread.hpp负责把线程封成对象,ThreadPool.hpp负责把线程管起来、把任务派下去。两块拼在一起,线程池的本体就立起来了。
3.2.1 Thread.hpp:线程封装
cpp
#include "Mutex.hpp"
#include "Task.hpp"
#include "Log.hpp"
namespace ThreadModule
{
int count = 1;
class Thread
{
private:
static void* Routine(void* args)
{
Thread* self = static_cast<Thread*>(args);
pthread_setname_np(pthread_self(), self->_name);
if (!self->_isrunning)
self->_isrunning = true;
self->_func(); // 转成 this->_func()
return nullptr;
}
public:
Thread(task_t task)
: _tid(0),
_func(task),
_isrunning(false)
{
snprintf(_name, sizeof(_name), "Thread-%d", count);
count++;
}
void Start()
{
if (_isrunning)
return;
int n = pthread_create(&_tid, nullptr, &Routine, this);
if (n == 0)
LogModule::LOG(LogModule::LEVEL::DEBUG) << "线程创建成功" << LogModule::CAGE;
else
LogModule::LOG(LogModule::LEVEL::ERROR) << "线程创建失败" << LogModule::CAGE;
}
void join()
{
pthread_join(_tid, nullptr);
}
char* Name()
{
return _name;
}
private:
pthread_t _tid;
bool _isrunning;
task_t _func;
char _name[128];
};
}
这个类把pthread那套裸接口包成了一个对象。几个关键点:
Routine是静态函数。 pthread_create只认void*(*)(void*)这个签名,成员函数自带隐式this,对不上。所以Routine必须是static,通过args把this传进来,再在里面调_func()。
线程名带编号。 用全局count给每个线程起名Thread-1、Thread-2......调试日志里一眼就能分辨谁是谁。
_isrunning在Routine里置位。 这一点很讲究,pthread_create成功,只代表线程被创建了,不代表它已经拿到CPU开始跑。_isrunning在Routine内部才被置为true,状态更新跟真实执行时机对得上。
Start里做了防重入。 已经跑起来的线程,不会再被Start一次。
3.2.2 ThreadPool.hpp:线程池核心实现
cpp
#ifndef __THREAD_POOL__
#define __THREAD_POOL__
#include "Log.hpp"
#include "Thread.hpp"
#include "Cond.hpp"
#include "Task.hpp"
#include <queue>
namespace ThreadPoolModuleBasedOnSingletonPattern
{
using namespace LogModule;
using namespace MutexModule;
using namespace ConModule;
using namespace ThreadModule;
static const size_t DEFULT_SIZE = 5;
template <typename T>
class ThreadPool
{
private:
void WakeUpAllThread()
{
if (_ThreadSleepSize)
_cond.Broadcast();
LOG(LEVEL::INFO) << "唤醒所有休眠的线程" << CAGE;
}
void WakeUpOneThread()
{
_cond.Signal();
LOG(LEVEL::INFO) << "唤醒一个休眠线程" << CAGE;
}
ThreadPool()
: _ThreadSleepSize(0),
_isrunning(false),
_Size(DEFULT_SIZE)
{
for (int i = 0; i < DEFULT_SIZE; i++)
{
_Thread.emplace_back(
[this]()
{
HanderTask();
});
}
}
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto& e : _Thread)
{
e.Start();
LOG(LEVEL::INFO) << "创建线程池成功" << e.Name();
}
}
ThreadPool<T>(const ThreadPool<T>& threadpool) = delete;
ThreadPool<T>& operator=(const ThreadPool<T>& threadpool) = delete;
public:
static ThreadPool<T>* GetInstance()
{
if (_SingPtr == nullptr)
{
MutexGuard mutexguard(_lock);
LOG(LEVEL::INFO) << "获取单例。。。" << CAGE;
{
if (_SingPtr == nullptr)
{
LOG(LEVEL::INFO) << "首次使用单例,创建单例" << CAGE;
_SingPtr = new ThreadPool<T>;
_SingPtr->Start();
}
}
}
return _SingPtr;
}
void HanderTask()
{
LOG(LEVEL::INFO) << "进入线程处理函数";
char name[64];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T task;
{
MutexGuard mutexguard(_mutex);
while (_isrunning && _TaskQueue.empty())
{
LOG(LEVEL::DEBUG) << "进入等待";
_ThreadSleepSize++;
_cond.Wait(_mutex);
_ThreadSleepSize--;
LOG(LEVEL::DEBUG) << "被唤醒";
}
LOG(LEVEL::INFO) << "退出等待 queue=" << _TaskQueue.size();
if (!_isrunning && _TaskQueue.empty())
{
LOG(LEVEL::INFO) << "线程池退出且任务队列为空" << CAGE;
break;
}
task = _TaskQueue.front();
LOG(LEVEL::INFO) << "取出任务";
_TaskQueue.pop();
}
task();
}
}
bool Equeue(const T& task)
{
if (_isrunning)
{
MutexGuard mutexguard(_mutex);
_TaskQueue.push(task);
if (_ThreadSleepSize > 0)
WakeUpOneThread();
return true;
}
return false;
}
void Stop()
{
LOG(LEVEL::DEBUG) << "线程池停止" << CAGE;
if (_isrunning)
{
MutexGuard mutexguard(_mutex);
_isrunning = false;
if (_ThreadSleepSize)
{
WakeUpAllThread();
}
}
}
void Join()
{
LOG(LEVEL::DEBUG) << "线程池回收线程" << CAGE;
if (!_isrunning)
{
for (auto& e : _Thread)
{
e.join();
}
}
}
private:
size_t _ThreadSleepSize;
bool _isrunning;
std::vector<Thread> _Thread;
std::queue<T> _TaskQueue;
size_t _Size;
Mutex _mutex;
Cond _cond;
static ThreadPool<T>* _SingPtr;
static Mutex _lock;
};
template <class T>
ThreadPool<T>* ThreadPool<T>::_SingPtr = nullptr;
template <class T>
Mutex ThreadPool<T>::_lock;
}
#endif
线程池本体
单例模式,双重检查锁。 GetInstance先在外层判断_SingPtr是否为空,空才去抢锁。抢到锁后再判一次,确认真的还没创建,才new出实例并调Start。外层判断省掉了实例创建之后的锁开销,内层判断挡住了首次并发时的重复创建。跟前面讲的懒汉模式,是一个路子。
构造函数只造对象,不启动线程。 ThreadPool的构造函数里,只负责往_Thread里塞Thread对象,每个对象绑一个HanderTask作为任务函数。真正启动线程的活儿,放在Start里,由GetInstance在首次创建后调用。这样设计,是为了让"创建"和"启动"分开,逻辑上更干净。
**HanderTask是工作线程的主循环。**每个线程跑起来之后,就在这个函数里转圈:
- 加锁,检查队列是否为空。空,就_ThreadSleepSize++,然后_cond.Wait(_mutex)挂起,把锁交出去。
- 被唤醒后,_ThreadSleepSize--,再看一眼队列和 _isrunning。
- 如果线程池已经停了,而且队列也空了,break 退出循环,线程结束。
- 否则,从队头取一个任务,解锁,执行 task()。
Equeue负责投递任务。 加锁,把任务推进队列,如果发现有线程在睡,就 WakeUpOneThread 叫醒一个。一放一取,配合默契。
**Stop和Join负责收尾。**Stop把_isrunning 置为false,然后唤醒所有睡着的线程,让它们检查到停止信号后退出。Join挨个join,把线程资源回收干净。
到这里,线程池的骨架就搭完了。Thread管单个线程的生老病死,ThreadPool管一群线程的调度和任务分发。单例模式保证了全局只有一个线程池,双重检查锁保证了它在多线程环境下也能安全地出生。
3.3 从编译到运行:构建与测试
代码写完了,得让它真正跑起来。一个 Makefile 管编译,一个 Main.cpp 管调用,两下配合,线程池就能转起来了。
3.3.1 Makefile:项目编译规则
bash
BIN = proc
SRC = Main.cpp
$(BIN) : $(SRC)
g++ -o $@ $^ -pthread -std=c++17
.PHONY: clean
clean :
rm -f $(BIN) ./log/log.txt
3.3.2 Main.cpp:线程池测试入口
cpp
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModuleBasedOnSingletonPattern;
const size_t NUM = 5;
int main()
{
ENABLE_FILE_LOG_STRATEGY();
for (int i = 0; i < NUM; i++)
{
sleep(1);
ThreadPool<task_t>::GetInstance()->Equeue(DownLoadTask);
}
ThreadPool<task_t>::GetInstance()->Stop();
ThreadPool<task_t>::GetInstance()->Join();
return 0;
}
主程序干的事很直白:把日志策略切到文件模式,然后每隔一秒,往线程池里丢一个下载任务,一共丢5个。丢完之后,调Stop让线程池停下,再调Join把线程回收干净。
几个细节值得留意:
ENABLE_FILE_LOG_STRATEGY()切的是日志输出方式。 这一宏一调,日志就从控制台转到了文件里。不调它,默认走的是控制台策略,日志直接打印到屏幕上。两种方式,各有用处------调试时看控制台,跑久了存文件。
GetInstance()每次都被调用,但线程池只创建一次。 第一次调用时,单例还没出生,GetInstance一路走到new,把线程池造出来,顺手启动所有工作线程。后面几次调用,外层判断一看_SingPtr非空,直接返回,连锁都不用碰。单例的甜头,就体现在这儿。
sleep(1)让任务一个个来,方便观察。 如果一口气把5个任务全丢进去,线程们会瞬间把任务抢光,日志刷得太快,看不出调度过程。加个一秒的间隔,生产一个、消费一个,节奏就清清楚楚了。
**测试时的日志输出,能帮你看清整个调度链路。**HanderTask里埋了不少日志:进入等待、被唤醒、取出任务、退出循环。跑一遍,控制台或日志文件里,线程们的一举一动全都有迹可循。哪个线程先醒、哪个任务先被取走、队列什么时候空、线程什么时候退,全都看得见。
如果这篇文章对你有帮助,别忘了点个赞、点个收藏、点个关注。你的每一次反馈,都是我继续硬核输出的最大动力。我们下篇见。