现在,我们将基于之前完成的封装来设计一个线程池。在正式编码前,需要做好以下准备工作:
这些准备工作我们已经做完了,下面我们就来设计一个线程池
核心概念与产生背景
线程池是一种基于池化技术(Pooling)管理线程的并发编程模型。其核心思想是:预先创建好一定数量的线程,放入一个“池子”中统一管理。当有任务需要执行时,不是直接创建一个新线程,而是从池中获取一个空闲线程来执行任务;任务执行完毕后,线程并不立即被销毁,而是返回池中等待执行下一个任务。
产生背景: 在早期并发模型中,“即时创建,即时销毁”的线程生命周期管理方式存在显著瓶颈:
线程池技术通过复用线程、控制并发数量、统一管理生命周期,完美地解决了上述问题,成为了高并发应用不可或缺的基础组件。
核心优势与价值
ScheduledThreadPoolExecutor)。
线程池的典型应用场景
1. 高并发短任务处理
典型示例:Web服务器请求处理
2. 实时性要求高的应用
典型示例:金融交易系统
3. 突发流量处理
典型示例:电商秒杀活动
不适合的场景:
线程池类型详解
1. 固定大小线程池(FixedThreadPool)
实现原理:
适用场景:
特点:
2. 可伸缩线程池(CachedThreadPool)
实现原理:
适用场景:
特点:
此处,我们选择固定线程个数的线程池。

这里我们实现线程池时,使用5个固定数量的线程
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include "Log.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
namespace ThreadPoolModule
{
using namespace ThreadModlue;
using namespace LogModule;
using namespace CondModule;
using namespace MutexModule;
static const int gnum = 5; // 预创建5个线程
template <class T>
class ThreadPool
{
public:
ThreadPool(int num = gnum)
: _num(num)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
t();
}
}
void Start()
{
for(auto& thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
};
}分析:
核心成员变量
_threads:std::vector<Thread> 类型,存储和管理工作线程对象
_num:整数类型,记录线程池中的线程数量
_taskq:std::queue<T> 类型,作为任务队列,存储待处理的任务
_cond 和 _mutex:条件变量和互斥锁,用于线程间同步和任务队列的线程安全访问
构造函数:构造函数接受一个整数参数num,表示线程池中线程的数量,默认值为gnum(5)。在构造函数中,我们创建了num个线程,并将每个线程的执行函数设置为HandlerTask(一个不断从任务队列中取任务并执行的函数)。这里使用了lambda表达式来包装HandlerTask。
成员函数HandlerTask:这是每个线程的工作函数。它在一个无限循环中不断从任务队列中取出任务并执行。在取任务时,需要先获取互斥锁,然后检查任务队列是否为空。如果为空,则调用条件变量的Wait方法等待;否则,从队列中取出一个任务,然后释放锁(通过LockGuard的作用域),接着执行任务。
注意:
t()),避免任务执行时间过长阻塞其他线程
Start函数:启动所有线程。遍历线程向量,调用每个线程的Start方法,并打印日志。
对于成员函数HandlerTask,我们不想被外部调用,我们可以将其私有
我们新增一个成员变量,作为运行标志位,线程池运行时为true,停止为false
static const int gnum = 5; // 预创建5个线程
template <class T>
class ThreadPool
{
private:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
//t();
}
}
public:
ThreadPool(int num = gnum)
: _num(num)
,_isrunning(false)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void Start()
{
if(_isrunning)
return;
_isrunning = true;
for(auto& thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
void Stop()
{
if(!_isrunning)
return;
_isrunning = false;
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
bool _isrunning; // 运行标志位
};成员函数HandlerTask,它在一个无限循环中不断从任务队列中取出任务并执行。
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空或者
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
//t();
}
}要么是在循环等任务,要么就是在执行任务,那我们要怎么退出呢?
我们先来分析一下,当我们的线程池退出时,也就是将运行标志位置为false,我们的线程处于什么状态呢?
可能是在等待,有可能在等待唤醒,也有可能在执行任务
所以我们要想线程池退出,不能只是简单的将所有线程停止或取消,我们应该让任务队列中的任务都被执行完了,并且运行标志位也被置为false,这时候才能让线程池退出,也就是说如果我们队列中还有任务,或者运行标志位为true,那我们就不能退出
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 线程被唤醒
// 判断线程池是否退出——如果线程池要退出,并且任务队列为空就退出
if(!_isrunning && _taskq.empty())
{
LOG(LogLevel::INFO) << name << "退出了,线程池想退出且任务队列为空";
break;
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
//t();
}
}但是如果我们的任务队列为空,此时所有线程都在条件变量Wait处等待唤醒,此时我们将线程池退出Stop,也就是将运行标志位置为false,那此时所有线程都会被阻塞在条件变量处休眠,等待被唤醒,那不就退出不了了吗?
所以我们线程池退出时还需要将那些在Wait的线程唤醒,判断条件也需要改,因为如果线程被唤醒,但是我们任务队列仍然为空,那就会再次进入循环继续Wait,但是我们线程池要退出呀,不能再让线程继续去Wait,所以还需要判断线程池是否退出
template <class T>
class ThreadPool
{
private:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空, 或者线程池没有退出,我们才需要wait
while (_taskq.empty() && _isrunning)
{
_sleepernum++;
_cond.Wait(_mutex);
_sleepernum--;
}
// 线程被唤醒
// 判断线程池是否退出——如果线程池要退出,并且任务队列为空就退出
if (!_isrunning && _taskq.empty())
{
LOG(LogLevel::INFO) << name << "退出了,线程池想退出且任务队列为空";
break;
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
// t();
}
}
void WakeUpAllThread()
{
LockGuard lockguard(_mutex);
if (_sleepernum)
{
_cond.Broadcast();
LOG(LogLevel::INFO) << "唤醒所有线程";
}
}
public:
ThreadPool(int num = gnum)
: _num(num), _isrunning(false)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
void Stop()
{
if (!_isrunning)
return;
_isrunning = false;
// 唤醒所有线程
WakeUpAllThread();
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
bool _isrunning; // 运行标志位
int _sleepernum; // 线程休眠的数量
};在学习了线程控制章节后,我们知道线程退出后,需要join等待线程退出,这里我们也需要等待,代码如下:
void Join()
{
for(auto& thread : _threads)
{
thread.Join();
}
}下面我们先来测试一下:
#include "Log.hpp"
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{
Enable_Console_Log_Strategy();
ThreadPool<int> *tp = new ThreadPool<int>();
tp->Start();
sleep(5);
tp->Stop();
tp->Join();
return 0;
}运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./threadpool
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-1
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-2
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-3
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-4
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-5
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [64] - 唤醒所有线程
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-2退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-1退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-3退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-4退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-5退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success那接下来就需要将任务入到任务队列中
bool Enqueue(const T& in)
{
LockGuard lockguard(_mutex);
// 如果线程池退出就不能再将任务入队列
if(_isrunning)
{
_taskq.push(in);
// 有线程在休眠,就唤醒
if(_sleepernum > 0)
{
_cond.Signal();
}
return true;
}
return false;
}那我们再来个任务试试,就和之前进程间通信时的任务一样,这里我们就只用一个任务来测试
#pragma once
#include <functional>
#include "Log.hpp"
using namespace LogModule;
// 定义了一个任务类型,返回值void,参数为空
using task_t = std::function<void()>;
void Download()
{
LOG(LogLevel::DEBUG) << "这是一个下载的任务...";
}下面我们再来测试一下
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{
Enable_Console_Log_Strategy();
ThreadPool<task_t> *tp = new ThreadPool<task_t>();
tp->Start();
int count = 10;
while(count--)
{
tp->Enqueue(Download);
sleep(1);
}
tp->Stop();
tp->Join();
return 0;
}运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./threadpool
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-1
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-2
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-3
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-4
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-5
[2025-09-12 17:31:52] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:53] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:54] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:55] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:56] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:57] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:58] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:59] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:32:00] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:32:01] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [64] - 唤醒所有线程
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-1退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-2退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-3退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-4退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-5退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式解决了需要全局唯一对象的场景,避免多个实例造成的资源浪费或状态不一致问题。
关键设计要点:
new 创建实例 。getInstance()):提供全局访问入口,控制实例的创建逻辑 。应用场景:配置文件加载、线程池管理、数据库连接池、Session 实现等需全局唯一资源的场景 。
(1)唯一性
(2)全局访问点
getInstance())提供统一访问入口,确保所有代码使用同一实例 。(3)资源优化
(4)线程安全挑战
生活实例:正如一个男人只能有一个媳妇(在一夫一妻制社会中),某些系统组件也只需要一个实例。
服务器开发应用:在很多服务器开发场景中,经常需要让服务器加载大量数据(上百GB)到内存中。例如电商平台的商品信息、社交网络的用户关系图等。此时往往要用一个单例的类来管理这些数据,避免重复加载造成内存浪费。
类比:
template <typename T>
class Singleton {
static T data; // 静态成员变量,在程序开始时初始化
public:
static T* GetInstance() {
return &data;
}
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default; // 构造函数私有化
~Singleton() = default; // 析构函数私有化
};关键点分析:
static T data 是静态成员变量,在程序启动时(main函数执行前)就完成初始化
GetInstance() 方法直接返回静态实例的地址,简单高效
template <typename T>
class Singleton {
static T* inst; // 静态指针,初始为nullptr
public:
static T* GetInstance() {
if (inst == nullptr) {
inst = new T(); // 第一次调用时创建实例
}
return inst;
}
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
// 初始化静态成员
template <typename T>
T* Singleton<T>::inst = nullptr;关键点分析:
inst 初始化为 nullptr
GetInstance() 时才创建实例
inst == nullptr,都可能通过检查,导致创建多个实例
new 创建实例,但没有相应的 delete 操作
#include <mutex>
template <typename T>
class Singleton {
volatile static T* inst; // volatile防止编译器过度优化
static std::mutex lock; // 互斥锁保证线程安全
public:
static T* GetInstance() {
if (inst == nullptr) { // 第一次检查,避免不必要的锁竞争
std::lock_guard<std::mutex> guard(lock); // RAII方式加锁
if (inst == nullptr) { // 第二次检查,确保只有一个线程创建实例
inst = new T();
}
}
return inst;
}
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
// 初始化静态成员
template <typename T>
volatile T* Singleton<T>::inst = nullptr;
template <typename T>
std::mutex Singleton<T>::lock;关键点分析:
if (inst == nullptr) 避免不必要的锁竞争
std::mutex 和 std::lock_guard 保证线程安全
lock_guard 采用 RAII 技术,自动管理锁的生命周期
inst = new T() 可能被重排序为:分配内存 → 赋值给 inst → 调用构造函数
volatile 是额外的保障
这种实现方式既保证了线程安全,又避免了不必要的锁竞争,是懒汉单例模式的经典实现。
线程池本身是系统关键资源,创建多个线程池实例会导致:
实现单例式线程池是为了统一管理线程资源、提高系统效率、保证稳定性,并提供一个简洁全局的并发编程接口。
下面我们实现线程安全的懒汉方式来实现单例式线程池
首先需要将构造函数和析构函数私有,Start函数也需要私有,同时拷贝构造和赋值重载需要显式删除
private:
ThreadPool(int num = gnum)
: _num(num), _isrunning(false)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
~ThreadPool() {}
// 删除拷贝构造函数和赋值运算符
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}定义静态指针,实现单例模式
class ThreadPool
{
...
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
bool _isrunning; // 运行标志位
int _sleepernum; // 线程休眠的数量
static ThreadPool<T>* _inst; // 单例指针
static Mutex _lock;
};
// 静态成员类外初始化
template<class T>
ThreadPool<T>* ThreadPool<T>::_inst = nullptr;
template<class T>
Mutex ThreadPool<T>::_lock;实现Getinstance函数
static ThreadPool* GetInstance()
{
if(_inst == nullptr)
{
LockGuard lockguard(_lock);
LOG(LogLevel::DEBUG) << "获取单例...";
if(_inst == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用,创建单例...";
_inst = new ThreadPool<T>();
_inst->Start();
}
}
return _inst;
}测试一下:
int main()
{
Enable_Console_Log_Strategy();
int count = 10;
while(count--)
{
ThreadPool<task_t>::GetInstance()->Enqueue(Download);
sleep(1);
}
ThreadPool<task_t>::GetInstance()->Stop();
ThreadPool<task_t>::GetInstance()->Join();
return 0;
}运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./threadpool
[2025-09-12 22:33:16] [DEBUG] [107860] [ThreadPool.hpp] [105] - 获取单例...
[2025-09-12 22:33:16] [DEBUG] [107860] [ThreadPool.hpp] [108] - 首次使用,创建单例...
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-1
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-2
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-3
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-4
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-5
[2025-09-12 22:33:16] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:17] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:18] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:19] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:20] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:21] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:22] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:23] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:24] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:25] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [95] - 唤醒所有线程
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-1退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-2退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-3退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-5退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-4退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success从运行结果可以看到没得问题,并且在使用日志向控制台输出后,也没有出现打印信息全都混在一起的情况。
线程安全:指多个线程同时访问共享资源时,能够正确执行而不会相互干扰或破坏彼此的执行结果。通常情况下,当多个线程并发执行仅包含局部变量的同一段代码时,不会产生不同的结果。但如果对全局变量或静态变量进行操作且未加锁保护,就容易出现线程安全问题。
重入:指同一个函数被不同执行流调用时,在前一个流程尚未执行完成时,又有其他执行流进入该函数。若一个函数在重入情况下仍能保持运行结果一致且不出现任何问题,则称为可重入函数,反之则为不可重入函数。
目前我们已经能够理解重入主要分为两种情况:
常见线程不安全的情况
• 未对共享变量进行保护的函数 • 函数状态随调用次数发生变化的函数 • 返回静态变量指针的函数 • 调用其他线程不安全函数的函数
常见不可重入的情况
• 调用malloc/free函数,因为它们使用全局链表管理堆内存 • 调用标准I/O库函数,因其实现常依赖不可重入的全局数据结构 • 函数内部使用了静态数据结构
常见线程安全的情况
• 仅读取全局/静态变量而无写入操作 • 类或接口提供原子性操作 • 多线程切换不会导致接口执行结果产生歧义
常见可重入的情况
• 不使用全局或静态变量 • 不使用动态内存分配(malloc/new) • 不调用不可重入函数 • 不返回静态/全局数据,所有数据由调用方提供 • 使用局部数据,或通过全局数据的本地副本来保护全局状态
不要被专业术语的复杂性吓到,通过仔细分析你会发现这些概念本质上是相互关联的。让我们深入探讨可重入函数与线程安全之间的关系。
可重入与线程安全的联系
基本对应关系
• 可重入函数必然线程安全:如果一个函数被设计为可重入的,那么它自然就是线程安全的。这是最核心的要点,掌握这一点就抓住了关键。
示例:一个只使用局部变量的纯计算函数,既可以被多个线程安全调用,也可以在信号处理程序中安全使用。
• 不可重入函数潜在风险:不可重入的函数不能被多个线程同时使用,否则可能引发数据竞争、内存污染等线程安全问题。
典型例子:使用静态缓冲区的strtok()函数,在多线程环境下会导致不可预期的结果。
• 全局变量的影响:使用全局变量的函数会同时丧失可重入性和线程安全性,因为全局状态会被所有调用者共享。
例如:一个使用static int counter来统计调用次数的函数,在多线程环境下计数会出错。
可重入与线程安全的区别
概念范围
• 包含关系:可重入函数是线程安全函数的一个子集。所有可重入函数都是线程安全的,但并非所有线程安全函数都是可重入的。
类比:就像所有正方形都是矩形,但并非所有矩形都是正方形。
• 锁机制的影响:
特别注意事项
应用场景考量
• 信号处理的影响:在大多数情况下,如果不考虑信号导致执行流重入的特殊情况,线程安全和可重入在安全性角度可以不做严格区分。
• 关注点差异:
实际开发建议
死锁是指在多进程/线程系统中的一种资源竞争状态,当一组进程中的每个进程都持有至少一个不可抢占的资源(即该资源在被使用过程中不会被系统强制收回),同时又在等待获取该组中其他进程所占用的资源时,就会形成环形等待链,导致所有相关进程都无法继续执行下去,系统进入永久阻塞的状态。
为便于说明,假设线程A和线程B必须同时获取锁1和锁2,才能继续访问后续资源

申请单把锁是原子操作,但申请多把锁则未必能保证原子性。

这个时候造成的结果是:

互斥条件(Mutual Exclusion):这是死锁产生的四个必要条件之一,指在并发环境中,一个资源(如打印机、共享内存、文件等)在同一时间只能被一个执行流(线程或进程)独占使用。当某个执行流已经获取该资源时,其他执行流必须等待,直到该资源被释放。这个条件保证了资源的独占性,但同时也可能导致死锁的发生。
请求与保持条件(Request and Hold Condition)也是死锁产生的四个必要条件之一,指的是在并发系统中,当一个执行流(如进程或线程)因为请求新的资源而被阻塞时,仍然保持着已获得的资源不放。这种状况会导致多个执行流相互等待对方释放资源,从而形成死锁。
具体来说,请求与保持条件包含两个关键方面:

不剥夺条件(Non-preemptive Condition) 也称为不可抢占条件。该条件要求一个进程在执行过程中已获得的资源,在未使用完毕之前,其他进程或系统不能强行剥夺或抢占该资源。只有在进程主动释放资源后,其他进程才能获取这些资源。
关键点说明

循环等待条件(Circular Wait Condition)是多线程编程或操作系统资源分配中常见的一种死锁情况。具体表现为:有多个执行流(线程或进程)同时运行,每个执行流都在等待其他执行流释放资源,而这些等待关系形成了一个闭合的环形链。

死锁通常发生在多个进程或线程互相等待对方释放资源时。预防死锁需要从资源分配和请求策略入手。
破坏互斥条件 某些资源可以通过共享方式使用,避免独占。例如,只读文件可以允许多个进程同时访问,减少竞争。
破坏占有并等待条件 进程在开始执行前必须一次性申请所有所需资源。如果无法满足,则暂时不分配任何资源。这种方式可能导致资源利用率降低。
破坏非抢占条件 如果进程无法获得额外资源,必须释放已占有的资源。这种策略适用于状态容易保存和恢复的资源,如CPU寄存器。
破坏循环等待条件 对资源类型进行线性排序,要求进程按照编号顺序申请资源。例如,进程只能先申请编号较小的资源,再申请编号较大的资源。
避免死锁的算法
银行家算法 通过模拟资源分配检查系统是否处于安全状态。每次资源分配前,算法会判断剩余资源是否能满足至少一个进程的最大需求,从而避免进入不安全状态。
资源分配图算法 通过维护资源分配图检测是否存在环路。如果图中没有环路,则系统不会发生死锁;若存在环路,则可能发生死锁。
检测与恢复策略
定期检测死锁 通过资源分配图或等待图算法定期扫描系统状态。一旦检测到死锁,立即采取恢复措施。
终止进程 强制终止一个或多个死锁进程,释放其占用的资源。可以选择终止代价最小的进程,例如运行时间最短或资源占用最少的进程。
资源抢占 从某些进程中抢占资源分配给其他进程。被抢占资源的进程可能需要回滚到之前的检查点重新执行。
实际应用建议
通过合理设计资源管理策略,可以显著降低死锁发生的概率。
STL容器是否具备线程安全性?
答案是否定的。
这是由于STL在设计时优先考虑性能优化,而线程安全所需的锁机制会显著影响性能表现。此外,不同容器类型(如哈希表的表锁与桶锁,锁整个表(粗粒度)或锁单个桶(细粒度))需要采用不同的加锁策略,性能影响也各不相同。
因此,STL默认不提供线程安全保障。若要在多线程环境中使用,开发者需要自行实现线程安全机制。
智能指针是否是线程安全的?
unique_ptr 是线程安全的,这是因为:
shared_ptr 的线程安全性更为复杂,主要体现在以下几个方面:
使用建议
1. 悲观锁
synchronized 关键字、ReentrantLock 等。
2. 乐观锁
3. CAS操作
4. 自旋锁
5. 读写锁
ReadWriteLock 接口及其实现 ReentrantReadWriteLock。
总结对比
锁类型 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
悲观锁 | 先加锁,再操作 | 保证强一致性,简单安全 | 性能开销大 | 写多读少,临界区操作耗时 |
乐观锁 | 先操作,更新时检查 | 性能高,无锁开销 | 存在ABA问题,竞争激烈时重试开销大 | 读多写少,竞争不激烈 |
CAS | 比较并交换(乐观锁的实现) | 硬件实现,高效 | ABA问题,自旋开销 | 实现原子操作,无锁数据结构 |
自旋锁 | 失败后循环尝试(悲观锁的实现) | 避免线程切换开销 | 占用CPU空转 | 锁持有时间极短的场景 |
读写锁 | 读共享,写独占 | 允许多线程并发读,大幅提升读性能 | 实现相对复杂,写操作可能饿死 | 读多写少的并发场景 |