目录
一、概述
线程池是一种非常重要的并发编程技术,它通过预先创建一组线程来处理任务,从而避免了频繁创建和销毁线程的开销,提高了程序的性能和资源利用率。本文将详细介绍一个基于C++和pthread库实现的线程池,包括其原理、实现细节、工作流程以及注意事项。
二、线程池的基本原理
线程池的核心思想是将线程的创建和管理与任务的执行分离。线程池预先创建一定数量的工作线程,这些线程在没有任务时处于空闲状态,等待任务的到来。当有任务提交到线程池时,线程池会从任务队列中取出一个任务并分配给一个空闲线程执行。任务执行完成后,线程会返回线程池,等待下一个任务。
线程池的主要优点包括:
-
减少线程创建和销毁的开销:线程的创建和销毁是相对耗时的操作,线程池通过复用线程,避免了频繁的线程创建和销毁
-
提高程序性能:线程池中的线程可以快速响应任务,减少了任务的等待时间
-
限制并发数量:线程池可以限制同时运行的线程数量,避免系统资源耗尽
-
提高资源利用率:通过合理管理线程,线程池可以更好地利用系统资源
线程池解决以下问题:
-
解决任务处理
-
阻塞IO
-
解决线程创建于销毁的成本问题
-
管理线程
三、线程池工作情景
假设我们的线程池有 3个线程,任务队列目前 不限制大小。
情况一:风平浪静,大家都在摸鱼
线程池已经启动,三个线程也到位了,但主线程什么任务也没push进来。任务队列是空的,三个线程蹲在那儿傻等,啥也不干。
情况二:任务来了,正好够分
主线程突然来了三个任务,向队列里push任务。三个线程立马被唤醒,一个一个冲出来取任务。任务队列瞬间被清空,三个线程各自扛着任务去干活了。主线程也轻松了,因为它没多扔任务,线程池刚好吃得下。
情况三:任务太多,先排队等
三个线程全在干活,结果主线程又扔进来一个任务。这时候线程池已经没空闲线程了,新任务只能乖乖进队列排队。等三个线程有人干完活了,就会主动从队列里再取一个任务继续干。这就是线程池的“先上岗,后排队”策略,妥妥的“活人等任务,任务等活人”。
情况四:任务爆仓,主线程被迫停摆
情况比较极端,先假设任务队列被我们设置了最大容量限制。这时候线程池的三个线程全在忙,队列也满。主线程又想扔任务进来,发现没地方放,也没人接,只能原地等待,等队列腾出位置。
四、线程池的实现细节
(一)数据结构设计
线程池的主要数据结构包括:
-
线程列表:存储线程池中所有线程的信息。
-
任务队列:存储待执行的任务。
-
互斥锁:用于保护任务队列的线程安全。
-
条件变量:用于实现线程的等待和唤醒机制。
以下是线程池的基本数据结构定义:
template <class T>
class ThreadPool
{
private:
std::vector<ThreadInfo> _threads; // 线程列表
std::queue<T> _tasks; // 任务队列
pthread_mutex_t _mutex; // 互斥锁
pthread_cond_t _cond; // 条件变量
static ThreadPool<T> *_tp; // 线程池实例指针
static pthread_mutex_t Instance_mutex; // 用于保护单例实例的互斥锁
};
// 初始化线程池实例指针为nullptr
template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
// 初始化单例保护互斥锁
template <class T>
pthread_mutex_t ThreadPool<T>::Instance_mutex = PTHREAD_MUTEX_INITIALIZER;
在本文的实现中,任务队列使用了std::queue,线程列表使用了std::vector,互斥锁和条件变量使用了pthread_mutex_t和pthread_cond_t。
(二)单例模式的实现
单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。单例模式有两种常见的实现方式:饿汉模式和懒汉模式。
饿汉模式
工作原理:饿汉模式在程序启动时就创建单例对象,确保对象在程序运行期间始终存在。 解决的问题:避免了懒汉模式中可能出现的线程安全问题,因为对象在程序启动时就已经创建,不会出现多线程同时创建对象的情况。 缺点:即使程序中从未使用过单例对象,也会占用内存。
懒汉模式
工作原理:懒汉模式在第一次访问单例对象时才创建对象,延迟了对象的创建时间。 解决的问题:节省了内存资源,只有在需要时才会创建对象。 缺点:需要额外的线程同步机制来确保多线程环境下的线程安全。
在本文的线程池实现中,我们使用了懒汉模式。以下是单例模式的实现代码:
template <class T>
class ThreadPool
{
public:
// 获取单例实例
static ThreadPool<T> *GetInstance()
{
// 双重检查锁定模式,确保线程安全
if (_tp == nullptr)
{
pthread_mutex_lock(&Instance_mutex);
if (_tp == nullptr)
{
_tp = new ThreadPool<T>();
}
pthread_mutex_unlock(&Instance_mutex);
}
return _tp;
}
private:
// 私有构造函数,防止外部直接构造
ThreadPool(int num = defaultnum)
: _threads(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
// 私有析构函数,防止外部直接析构
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
// 禁止拷贝构造和赋值操作
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(ThreadPool<T> &) = delete;
// 静态成员变量
static ThreadPool<T> *_tp; // 线程池实例指针
static pthread_mutex_t Instance_mutex; // 用于保护单例实例的互斥锁
};
代码注释
-
GetInstance方法:-
首先检查单例对象是否已经创建,如果未创建,则加锁。
-
在加锁后再次检查,确保只有一个线程能够创建对象。
-
创建对象后解锁,返回单例对象。
-
-
_tp:静态指针,存储单例对象。 -
Instance_mutex:静态互斥锁,用于保护单例对象的创建过程。
(三)线程池的初始化
线程池的初始化包括以下步骤:
-
初始化互斥锁和条件变量。
-
创建线程列表。
-
启动线程池中的每个线程。
在代码中,线程池的初始化通过构造函数和Start方法完成。构造函数负责初始化互斥锁和条件变量,并创建线程列表。Start方法负责启动线程池中的每个线程。
template <class T>
class ThreadPool
{
// 启动线程池中的所有线程
void Start()
{
int num = _threads.size(); // 获取线程池中线程的数量
for (int i = 0; i < num; i++) // 遍历线程列表
{
_threads[i].name = "thread-" + std::to_string(i + 1); // 设置线程名称
pthread_create(&(_threads[i].tid), nullptr, HandleTask, this); // 创建线程
}
}
};
线程创建函数 pthread_create 要求传一个 void* (*)(void*) 类型的函数,但 C++ 类的成员函数默认带了个 this 指针,因此我们要使用static 函数。static 函数没有 this 指针,但我们可以手动传个 this 进去,然后调用真正的成员函数,就像“代理登录”。
(四)任务的提交与执行
任务的提交和执行是线程池的核心功能。当有任务提交到线程池时,线程池会将任务加入任务队列,并唤醒一个等待中的线程来执行任务。任务执行完成后,线程会返回线程池,等待下一个任务。
在代码中,任务的提交通过Push方法完成,该方法将任务加入任务队列,并调用Wakeup方法唤醒一个等待中的线程。任务的执行通过HandleTask函数完成,该函数从任务队列中取出任务并执行。
template <class T>
class ThreadPool
{
// 线程池中线程的处理任务函数
static void *HandleTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); // 将参数转换为线程池指针
std::string name = tp->GetThreadName(pthread_self()); // 获取当前线程的名称
// 线程主循环,不断从任务队列中获取任务并执行
while (true)
{
tp->Lock(); // 加锁,保护任务队列
while (tp->IsQueueEmpty()) // 如果任务队列为空
{
tp->Sleep(); // 线程进入等待状态
}
T t = tp->Pop(); // 从任务队列中取出一个任务
tp->Unlock(); // 解锁,释放任务队列
t(); // 执行任务
std::cout << name << " run, " << "result: " << t.GetResult() << std::endl; // 输出任务执行结果
}
return nullptr; // 线程函数返回值
}
};
(五)线程同步机制
线程池中使用了互斥锁和条件变量来实现线程同步。互斥锁用于保护任务队列的线程安全,条件变量用于实现线程的等待和唤醒机制。
在代码中,Lock和Unlock方法用于加锁和解锁,Sleep方法用于线程等待,Wakeup方法用于线程唤醒。
template<class T>
class ThreadPool
{
public:
// 加锁操作,保护任务队列
void Lock()
{
pthread_mutex_lock(&_mutex);
}
// 解锁操作,释放任务队列
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
// 唤醒一个等待中的线程
void Wakeup()
{
pthread_cond_signal(&_cond);
}
// 线程进入等待状态
void Sleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
// 检查任务队列是否为空
bool IsQueueEmpty()
{
return _tasks.empty();
}
};
细节补充:
-
条件变量的使用:
pthread_cond_wait需要与互斥锁配合使用,确保线程在等待时不会出现竞态条件。 -
唤醒机制:
pthread_cond_signal只会唤醒一个等待中的线程,如果需要唤醒所有等待中的线程,可以使用pthread_cond_broadcast。
五、线程池的工作流程
线程池的工作流程如下:
-
线程池初始化:创建线程池实例,初始化互斥锁和条件变量,创建线程列表。
-
任务提交:调用
Push方法将任务加入任务队列,并唤醒一个等待中的线程。 -
任务执行:线程从任务队列中取出任务并执行,执行完成后返回线程池等待下一个任务。
-
线程等待:当任务队列为空时,线程进入等待状态,直到有新任务加入。
-
线程唤醒:当有新任务加入任务队列时,调用
Wakeup方法唤醒一个等待中的线程。
六、完整代码实现
以下是线程池的完整代码实现:
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
struct ThreadInfo
{
pthread_t tid;
std::string name;
};
static const int defaultnum = 5;
template <class T>
class ThreadPool
{
// 线程池中线程的处理任务函数
static void *HandleTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args); // 将参数转换为线程池指针
std::string name = tp->GetThreadName(pthread_self()); // 获取当前线程的名称
// 线程主循环,不断从任务队列中获取任务并执行
while (true)
{
tp->Lock(); // 加锁,保护任务队列
while (tp->IsQueueEmpty()) // 如果任务队列为空
{
tp->Sleep(); // 线程进入等待状态
}
T t = tp->Pop(); // 从任务队列中取出一个任务
tp->Unlock(); // 解锁,释放任务队列
t(); // 执行任务
std::cout << name << " run, " << "result: " << t.GetResult() << std::endl; // 输出任务执行结果
}
return nullptr; // 线程函数返回值
}
public:
// 获取单例实例
static ThreadPool<T> *GetInstance()
{
// 双重检查锁定模式,确保线程安全
if (_tp == nullptr)
{
pthread_mutex_lock(&Instance_mutex);
if (_tp == nullptr)
{
_tp = new ThreadPool<T>();
}
pthread_mutex_unlock(&Instance_mutex);
}
return _tp;
}
// 启动线程池中的所有线程
void Start()
{
int num = _threads.size(); // 获取线程池中线程的数量
for (int i = 0; i < num; i++) // 遍历线程列表
{
_threads[i].name = "thread-" + std::to_string(i + 1); // 设置线程名称
pthread_create(&(_threads[i].tid), nullptr, HandleTask, this); // 创建线程
}
}
// 向任务队列中添加一个任务
void Push(const T &t)
{
Lock(); // 加锁,保护任务队列
_tasks.push(t); // 将任务加入队列
Wakeup(); // 唤醒一个等待中的线程
Unlock(); // 解锁,释放任务队列
}
// 从任务队列中取出一个任务
T Pop()
{
T t = _tasks.front();
_tasks.pop();
return t;
}
// 加锁操作,保护任务队列
void Lock()
{
pthread_mutex_lock(&_mutex);
}
// 解锁操作,释放任务队列
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
// 唤醒一个等待中的线程
void Wakeup()
{
pthread_cond_signal(&_cond);
}
// 线程进入等待状态
void Sleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
// 检查任务队列是否为空
bool IsQueueEmpty()
{
return _tasks.empty();
}
// 获取线程名称
std::string GetThreadName(pthread_t tid)
{
std::string res = "\0";
for (const auto &t : _threads)
{
if (t.tid == tid)
res = t.name;
}
return res;
}
private:
// 私有构造函数,防止外部直接构造
ThreadPool(int num = defaultnum)
: _threads(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
// 私有析构函数,防止外部直接析构
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
// 禁止拷贝构造和赋值操作
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(ThreadPool<T> &) = delete;
// 静态成员变量
static ThreadPool<T> *_tp; // 线程池实例指针
static pthread_mutex_t Instance_mutex; // 用于保护单例实例的互斥锁
// 线程池的成员变量
std::vector<ThreadInfo> _threads; // 线程列表
std::queue<T> _tasks; // 任务队列
pthread_mutex_t _mutex; // 互斥锁
pthread_cond_t _cond; // 条件变量
};
// 初始化静态成员变量
template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::Instance_mutex = PTHREAD_MUTEX_INITIALIZER;
代码注释
-
GetInstance方法:-
使用双重检查锁定模式(Double-Checked Locking)确保线程安全。
-
首先检查单例对象是否已经创建,如果未创建,则加锁。
-
在加锁后再次检查,确保只有一个线程能够创建对象。
-
创建对象后解锁,返回单例对象。
-
-
Start方法:-
遍历线程列表,为每个线程设置名称并启动线程。
-
-
Push方法:-
加锁保护任务队列,将任务加入队列,唤醒一个等待中的线程,解锁释放任务队列。
-
-
Pop方法:-
从任务队列中取出一个任务。
-
-
Lock、Unlock、Wakeup、Sleep方法:-
提供线程同步机制,确保任务队列的线程安全。
-
-
GetThreadName方法:-
通过线程ID获取线程名称。
-
七、注意事项
-
线程安全:任务队列的访问必须加锁,确保线程安全。
-
资源管理:线程池的生命周期管理需要谨慎,避免资源泄漏。
-
任务类型:任务类型
T必须是可调用对象,并且需要提供GetResult方法。 -
线程数量:线程池的大小需要根据系统资源和任务类型合理配置。
-
异常处理:任务执行过程中可能抛出异常,需要在
HandleTask中处理异常,避免线程崩溃。
八、总结
线程池是一种非常重要的并发编程技术,它通过预先创建一组线程来处理任务,从而避免了频繁创建和销毁线程的开销,提高了程序的性能和资源利用率。本文详细介绍了线程池的基本原理、实现细节、工作流程以及注意事项,并给出了一个基于C++和pthread库的线程池实现。通过合理使用线程池,可以显著提升并发程序的性能和稳定性。
1128

被折叠的 条评论
为什么被折叠?



