目录

前言
在多线程编程中,线程池作为一种经典的线程使用模式,能够显著提升系统性能并优化资源管理。然而,要设计一个高效、稳定的线程池并非易事------它涉及到线程同步、资源管理、设计模式等多个核心概念。
本文将从线程池的基本原理出发,从理论到实践,逐步构建一个完整的单例模式线程池,并深入分析其中的技术细节与陷阱。
一、线程池是什么?
有关什么是线程池,其实一句话就能概括了------线程池是一种线程使用模式。
1.为什么要有线程池
我们知道Linux中的线程实际是"轻量级进程"模拟的,pthread_create在创建线程时底层会像fork 一样调用clone系统调用,既然是系统调用就伴随着执行流运行状态的转换,从用户态到内核态在到用户态,这样一来如果创建的线程过多会带来额外的开销,并且线程间的调度由于会切换CPU运行上下文等导致缓存Cache 收到影响,进而影响整体性能。
比如,假设系统突然收到很多任务但单个任务量都很小 的时候,如果没有线程池的话,将临时产生大量线程 ,虽然操作系统理论上能创建非常多的线程,但短时间内产生大量线程会使系统增加大量开销而且内存可能会到达极限,容易出现错误甚至系统崩溃。

而引入线程池的目的就是为了尽量较少上述损耗。线程池里维护着多个线程,随时等待着上层分配可并发执行的任务。这样第一避免了在处理短任务时创建与销毁线程的代价;第二线程池还能防止过分调度,尽量使得内核得到的充分利用。
线程池中可用线程数量应该取决于可用的处理器内核、网络sockets等系统资源的数量,不能盲目堆高,否则会加大管理成本,并占用较多系统资源。
2.单例模式
什么是单例模式
单例模式是一种设计模式。"单" = 唯一,只有一个的意思;"例" = 实例,即对象。合起来就是"唯一的实例"意思。
通过单例模式设计的个类在整个出现种只能有一个实例,并提供一个全局访问函数来访问该实例。
单例模式通常包括以下特点:
私有化或者删除构造函数,防止外部直接创建实例。
提供一个静态方法或静态变量来获取这个唯一实例。
为什么要有单例模式
在某些场景中,一个程序在创建对象时需要从磁盘中加载大量的数据到内存中,而这些数据在之后是被所有程序共享的,这种情况下就需要确保该类只有一个实例------这些数据只有一份,用以避免多个实例对象造成资源浪费或内存中数据的状态不一致。 此时往往要用一个单例模式设计的类来管理这些数据。
而线程池就非常适合用单例模式实现 ,我们使用线程池的一大目的就是为了更好的管理各类线程,并且我们使用线程池的目的不是线程池本身而是其中的众多线程,所有线程池本身在内存中往往也只用存在一个就可以了,避免过度开销和资源浪费。
单例模式的实现方式
经典的单列模式有两种实现方式,一种是懒汉模式,另一种是饿汉模式,这两种模式的核心区别是对象创建的时机。
懒汉模式
什么是懒汉模式?假设你是一个"懒汉",每次吃完饭都不洗碗,而是等下次吃饭前再洗碗。这其中核心行为-洗碗,是每每要到使用时才会做。这放到计算机视角,就是每每临到要使用某种资源时,才临时从磁盘中将数据调入内存 ,**这就是懒汉模式。**操作系统中也十分广泛的使用懒汉模式的思想设计,其中最典型的例子就是缺页中断。
**一句话总结:懒汉模式核心的思想就是资源延时加载,时间换空间。**这在一些需要快速启动程序的情况下十分常用,因为它将相较于CPU运行速度二元缓慢的从磁盘加载数据到内存的操作放在了后面等到使用时再加载,从而优化了程序的启动速度。
饿汉模式
什么是饿汉模式?假设你又成了一个"饿汉",每次吃完饭后都会把碗洗干净,只为下一次吃饭时尽早吃上饭。这就与懒汉模式行为刚好相反,放在计算机视角就是启动程序时立即就将之后会用到的资源读入内存,好处是在想要执行某复杂功能时能迅速运行,但坏处是浪费内存资源,原因是距离这些数据在加载到内存到真正使用它们时,可能间隔不少时间,这里的时间是站在CPU的视角看待的,在这些时间中被占用的内存完全可以用来做其他功能。
一句话总结:饿汉模式核心的思想就是资源立即加载,空间换时间。
这两种设计模式本没有优劣之分,完全是看使用场景,在以前计算机内存等资源稀缺时"懒汉模式"就收到欢迎,而现如今一些更追求即时执行的情况下"饿汉模式"可能又更受追捧。
本文采用懒汉模式模拟实现线程池。
二、代码实现单例式线程池
代码说明:
①用一个队列存储上层传递的任务;
②在创建线程后让每个线程执行HandlerTask函数拿任务执行任务,但pthread_create函数要求传入的线程执行函数只能是void*(void*)类型,如果HandlerTask不加static则会默认携带this指针,于是通过将this指针以参数的形式传入HandlerTask函数,在内部强转以访问内部成员;
③实现单例关键:如何判断是不是第一次申请线程池,通过初始化指针*_inc为nullptr,当GetInstance第一次申请时判断if(!*_inc)为真才能进入申请,将申请得到的线程池对象地址付给_inc,否则直接返回_inc(已经保持有线程池地址);
④为防止多执行流并发访申请单例,创建一个全局锁_inc_lock,以确保在GetInstance中互斥访问_inc。
cpp
#ifndef __ThreadPool__
#define __ThreadPool__
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <vector>
#include <queue>
const int defasltnums = 4;
namespace karsen_ThreadPool
{
// 用于单例线程池互斥申请
static pthread_mutex_t _inc_lock = PTHREAD_MUTEX_INITIALIZER;
template <class T>
class ThreadPool
{
private:
bool TaskEmpty() const
{
return _taskque.empty();
}
static void *HandlerTask(void *args)
{
ThreadPool *t = static_cast<ThreadPool *>(args);
while (true)
{
T task;
pthread_mutex_lock(&t->_mutex);
while (t->TaskEmpty() && t->_isrunning)
{
t->_sleep_nums++;
pthread_cond_wait(&t->_cond, &t->_mutex);
t->_sleep_nums--;
}
if (t->TaskEmpty() && !t->_isrunning)
{
std::cout << "线程池退出,且队列中无任务" << std::endl;
pthread_mutex_unlock(&t->_mutex);
break;
}
task = t->_taskque.front();
t->_taskque.pop();
pthread_mutex_unlock(&t->_mutex);
// 执行任务
task();
}
return nullptr;
}
bool WakeOne()
{
int n = pthread_cond_signal(&_cond);
if (n != 0)
{
std::cerr << __FILE__ << ' ' << __LINE__ << " :WakeOne faild " << std::endl;
return false;
}
std::cout << "唤醒一个线程" << std::endl;
return true;
}
bool WakeAll()
{
int n = pthread_cond_broadcast(&_cond);
if (n != 0)
{
std::cerr << __FILE__ << ' ' << __LINE__ << " :WakeAll faild " << std::endl;
return false;
}
std::cout << "唤醒所有线程" << std::endl;
return true;
}
// 构造函数私有化,导致外部无法显式创建对象
ThreadPool(int thread_nums = defasltnums)
: _thread_nums(thread_nums),
_sleep_nums(0),
_isrunning(false)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
public:
static ThreadPool<T> *GetInstance(int thread_nums = defasltnums)
{
if (_inc == nullptr)
{
pthread_mutex_lock(&_inc_lock);
std::cout << "获取单例线程池";
if (_inc == nullptr)
{
std::cout << "第一次获取线程池,创建线程池" << std::endl;
_inc = new ThreadPool<T>(thread_nums);
_inc->Start();
}
pthread_mutex_unlock(&_inc_lock);
}
return _inc;
}
bool Enqueue(const T &task)
{
pthread_mutex_lock(&_mutex);
if (!_isrunning)
{
std::cout << "线程池未启动" << std::endl;
pthread_mutex_unlock(&_mutex);
return false;
}
_taskque.push(task);
pthread_mutex_unlock(&_mutex);
if (_sleep_nums > 0)
WakeOne();
return true;
}
bool Start()
{
if (_isrunning)
return false;
_isrunning = true;
for (int i = 0; i < _thread_nums; ++i)
{
pthread_t id;
int n = pthread_create(&id, nullptr, HandlerTask, this);
_threads.push_back(id);
if (n != 0)
{
std::cerr << __FILE__ << ' ' << __LINE__ << " :Start faild " << std::endl;
return false;
}
}
std::cout << "ThreadPool start success" << std::endl;
return true;
}
bool Stop()
{
pthread_mutex_lock(&_mutex);
_isrunning = false;
pthread_mutex_unlock(&_mutex);
WakeAll();
return _isrunning == false;
}
bool Join()
{
for (auto &id : _threads)
{
int n = pthread_join(id, nullptr);
if (n != 0)
{
std::cerr << __FILE__ << ' ' << __LINE__ << " :Join faild " << std::endl;
std::cerr << std::strerror(n) << std::endl;
return false;
}
}
std::cout << "ThreadPool join success" << std::endl;
return true;
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
pthread_mutex_destroy(&_inc_lock);
}
private:
std::vector<pthread_t> _threads;
std::queue<T> _taskque;
int _sleep_nums;
int _thread_nums;
bool _isrunning;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
// 用于单例
static ThreadPool<T> *_inc;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::_inc = nullptr;
}
#endif
测试代码示例:
cpp
#include <iostream>
#include <functional>
#include <unistd.h>
#include "ThreadPool.hpp"
using namespace karsen_ThreadPool;
void Task_Upload()
{
std::cout << "正在执行,这是一个上传任务" << std::endl;
// 模拟耗时
sleep(2);
}
void Task_Download()
{
// 模拟耗时
sleep(2);
std::cout << "正在执行,这是一个下载任务" << std::endl;
}
int main()
{
using task_t = std::function<void()>;
ThreadPool<task_t> *test_thread_pool = ThreadPool<task_t>::GetInstance(10);
if (!test_thread_pool)
{
std::cerr << "error GetInstance " << std::endl;
exit(-1);
}
int cnt = 10;
while (cnt--)
{
sleep(1);
test_thread_pool->Enqueue(Task_Download);
test_thread_pool->Enqueue(Task_Upload);
}
std::cout << "*********停止线程池*********" << std::endl;
test_thread_pool->Stop();
test_thread_pool->Join();
return 0;
}
若读者还有疑问,欢迎在评论区指出,笔者看到后会第一时间回答。
三、死锁与线程安全
1.什么是死锁
死锁是指在一组进程或线程中,每个进程或线程都占有了不会释放的资源的同时 ,又互相申请被其他进程或线程所 占用的资源 ,从而导致谁也无法继续往后执行进而处于永久等待的一种情况。
比如两个线程,线程A 线程B都想要访问某临界资源,但该临界资源需要具备两种锁才能访问,线程申请锁的操作是原子的,但申请一把锁是原子的,但是申请两把锁就不一定了。而线程A与线程B各占有一把锁,在不放手自己资源的同时又索要对方的锁,这就造成了死锁。

2.如何避免死锁
首先死锁的产生需要满足四个必要条件,分析上述情况,可以总结出死锁产生的必要条件包括:
①互斥条件:一个公共资源每次只能被一个执行流使用;
②请求与保持条件:一个执行流因请求资源而导致自己阻塞,并且陷入阻塞时对已获得的资源保持不放;
③不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能被其他执行流强行剥夺;
④循环等待条件:几个执行流在满足上述条件之后,彼此之间形成一种头尾相接的循环等待资源的情况。
而避免死锁的方法就是打破上述必要条件之一,只要不满足四种条件中的任意一种,死锁就无法形成。 其中最常见的做法是打破请求与保持条件:在设计时就要求线程资源一次性分配,在线程发现自己无法访问临界资源时,释放自己拥有的所有资源再阻塞。
避免死锁还有一些成熟的算法可选,比如银行家算法、死锁检测算法等。
4.STL、智能指针以及可重入函数与线程安全
什么是线程安全?
就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果,这就是线程安全。
STL与线程安全
STL在设计之初由于追求极致的效率,而加锁解锁过程耗时耗力所以就没有考虑线程安全的问题,比如一个STL:: vector容器可以在同时被任意执行流访问,所以STL是线程不安全的,如果需要在多线程环境下使用STL容器,需要自行保证线程安全。
智能指针与线程安全
智能指针,我们只讨论最常用的unique_ptr与shared_ptr。尽管unique_ptr不支持拷贝,是"私有"的,但如果多个执行流并发访问同一个unique_ptr对象依旧不是线程安全的 ;而shared_ptr的引用计数是原理上是存在线程安全隐患的,但设计者在涉及之处也考虑到了这个问题,所以shared_ptr中的引用计数是原子操作 的,所以多个线程可以同时拷贝同一个shared_ptr对象 而不会导致引用计数错误,但是多**个执行流并发访问一个shared_ptr对象依旧不是线程安全的,**仍然需要同步。
可重入函数与线程安全
什么是可重入函数?
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流就再次进入的情况,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何问题,则该函数被称为可重入函数,否则,是不可重入函数。常见的比如信号导致一个执行流重复进入函数。
可重入与线程安全的关系总结成一句话:可重入函数是线程安全的,但线程安全的函数不一定可重入。
总结
本文首先介绍了线程池的概念,即是一种线程使用模式,通过维护多个线程来减少频繁创建和销毁线程的开销,提高系统性能。
之后介绍了单例模式,通过确保一个类只有一个实例,适用于线程池等资源管理场景。此外还介绍了线程池的必要性、单例模式的两种实现方式(懒汉模式和饿汉模式),并提供了基于懒汉模式的线程池代码实现。
最后讨论了死锁的四个必要条件及其避免方法,以及STL、智能指针的线程安全性问题。最后指出可重入函数是线程安全的,但线程安全函数不一定可重入。
读者水平有限,文中若有缺失错误之处,万望读者指出,共同进步~
读完点赞,手留余香~