线程池(Thread Pool)是一种多线程管理技术,用于提高程序中多线程的执行效率和资源利用率。 具体来说,线程池在程序启动时预先创建一定数量的线程,这些线程处于空闲等待状态。当有任务到来时,线程池从空闲线程中分配一个线程来执行任务,执行完后线程不会被销毁,而是继续回到线程池中等待下一次任务。这样避免了频繁创建和销毁线程带来的性能开销。
可能大家又会有这样的疑问:线程池为啥提高效率,他提前创建一定数量的线程池操作系统要维护它这不是要花费一定的成本吗?
总结:线程池的维护成本是固定且有限的,而节省的线程创建销毁开销及调度管理开销通常远大于维护成本,最终整体提升了系统的性能和效率。
线程池的主要优点包括:
例如Web服务器、数据库服务器、文件服务器等,需要同时处理大量客户端请求,通过线程池复用线程,减少线程创建销毁开销,提高响应速度和吞吐量。
后台任务处理、日志写入、消息队列消费等场景,线程池可以异步执行任务,提高主线程的响应性能,避免阻塞。
定时执行周期性任务时,使用线程池管理执行线程,保证资源利用率和任务调度的稳定性。
想一想在设计线程池之前,我们需要什么变量。
通过上述思考可以得到如下的伪代码:
template <typename T>
class ThreadPool
{
private:
std::vector<Thread> _threads; // 插入的lamada表达式会构建Thread类对象
int _num; // 线程池中的线程个数
std::queue<T> _taskq;
Cond _cond;
Mutex _mutex;
bool _isrunning;
int _sleepernum;
};
接下来需要创建线程池对象,创建一定数量的线程,并将该线程需要的函数传给指定的线程。而构造函数就可以完成该功能,为了支持泛型编程,我们设计成模版。伪代码如下:
static const int gnum = 5;
template <typename T>
class ThreadPool
{
private:
ThreadPool(int num = gnum)
: _num(num),
_isrunning(false),//线程还未启动
_sleepernum(0)//线程休眠个数
{
for (int i = 0; i < num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
};
该构造函数在创建thread对象的时候,还会将HandlerTask()函数赋值至自己的成员变量_func中,完成回调功能。
清理资源,做任何事有始有终。析构函数完成该功能。伪代码如下:
template <typename T>
class ThreadPool
{
public:
~ThreadPool()
{
}
};
往任务队列入任务,如何入任务,已经启动的线程才给他入任务,没有启动的线程给它入任务干嘛,他又不做事,为了保持原子性防止一个任务被多个线程执行,咱们直接加锁,如果线程都在休眠,需要手动唤醒一个线程去处理任务,通过上述思考,得到的伪代码如下:
template <typename T>
class ThreadPool
{
public:
bool Equeue(const T &in)
{
if (_isrunning)
{
LockGuard lockguard(_mutex);
_taskq.push(in);
if (_threads.size() == _sleepernum)
WakeUpOne();
return true;
}
return false;
}
};
如何启动线程池,将所有线程对象调用pthread_create(),创建线程,建立虚拟地址空间的映射,启动线程前,需要将_isrunning的状态修改为true,因为默认是false,这会影响回调函数处理任务的逻辑,伪代码如下:
template <typename T>
class ThreadPool
{
public:
void Start()
{
if (_isrunning)
return; // 线程已启动直接返回即可
_isrunning = true; // 不可省略,会导致任务不会被处理
for (auto &thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "start new thread success:" << thread.Name();
}
}
};
停止及等待线程池,伪代码如下,直接调用接口就行,特别需要注意,在停止所有线程前,需将_isrunning的状态设置为false,方便回调函数将该线程从while跳出,唤醒所有休眠的线程,直接同样的逻辑,伪代码如下:
void Stop()
{
if (!_isrunning)
return;
_isrunning = false;
// 唤醒所有休眠的线程
WakeUpAllThread();
}
void Join()
{
for (auto &thread : _threads)
{
thread.Join();
}
}
唤醒一个和所有休眠线程,直接调用接口即可,伪代码如下:
void WakeUpAllThread()
{
LockGuard lockguard(_mutex);
if (_sleepernum > 0)
_cond.Broadcast();
LOG(LogLevel::INFO) << "唤醒所有的休眠线程";
}
void WakeUpOne()
{
_cond.Signal();
LOG(LogLevel::INFO) << "唤醒一个的休眠线程";
}
线程池中的线程执行HandlerTask()函数,伪代码如下:
template <typename T>
class ThreadPool
{
public:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
while (_taskq.empty() && _isrunning)
{
_sleepernum++;
_cond.Wait(_mutex);
_sleepernum--;
}
// 内部线程被唤醒
if (!_isrunning && _taskq.empty())
{
LOG(LogLevel::INFO) << name << " 退出了,线程池退出&&任务队列为空";
break;
}
// 一定有任务
t = _taskq.front();
_taskq.pop();
}
t(); // 任务已经是私有的,不需要加锁
}
}
};
单例模式(Singleton Pattern)是一种设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。单例模式主要解决的是保证在整个应用程序中,某个类只能有一个对象实例,并且该实例可以被全局访问。
只允许存在一个类对象实例,所以要将类的构造函数私有化,将构造函数和赋值运算符禁用,外部就无法创建该类的对象了。如:
namespace A
{
class B
{
private:
B(std::string name):_name(name)
{}
std::string _name;
};
}
我们现在尝试创建对象,如下图看看有什么问题?
从图中可以看出类的外部不允许创建该类的对象。类的内部是可以创建对象的,只需要创建一个指向该类对象的静态指针或者静态对象,在初始化即可,因为外部无法访问该指针,可以提供一个静态的方法获取单例对象的句柄。下面展示两种方法实现该原理:
template <typename T>
class EagerSingleton {
private:
static T instance; // 静态实例(直接初始化)
// 私有化构造函数/析构函数
EagerSingleton() = default;
~EagerSingleton() = default;
public:
// 删除拷贝构造和赋值运算符
EagerSingleton(const EagerSingleton&) = delete;
EagerSingleton& operator=(const EagerSingleton&) = delete;
static T& GetInstance() {
return instance; // 直接返回已存在的实例
}
};
// 静态成员变量初始化(需在头文件外或模板特化中定义)
template <typename T>
T EagerSingleton<T>::instance;
当创建该类对象时,直接创建静态实例,不管它需不需要使用,这就是饿汉模式,可以看出该设计模式浪费空间延迟服务启动,所以需要改进,这就出现了懒汉模式。
**#include <iostream>
#include <mutex>
template <typename T>
class LazySingleton {
private:
static T* instance; // 静态指针(不直接创建对象)
static std::mutex mtx; // 互斥锁(线程安全)
// 私有化构造函数/析构函数
LazySingleton() = default;
~LazySingleton() = default;
public:
// 删除拷贝构造和赋值运算符
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static T* GetInstance() {
// 双重检查锁定(Double-Checked Locking)
if (!instance) {
std::lock_guard<std::mutex> lock(mtx);
if (!instance) {
instance = new T();
// 注册析构函数(防止内存泄漏)
static std::atexit([] {
delete instance;
instance = nullptr;
});
}
}
return instance;
}
};
// 静态成员变量初始化(需在头文件外或模板特化中定义)
template <typename T>
T* LazySingleton<T>::instance = nullptr;
template <typename T>
std::mutex LazySingleton<T>::mtx;
当需要该类对象才创建对象,可以看出当真正需要时,才创建对象,核心思想就是延时加载,有点类似于动态库的加载,也是不全部加载,当真正需要某些方法时才绑定关联关系。注意:静态成员变量需要在类外部进行初始化。
线程安全(Thread Safety)是指多个线程同时访问某个代码片段时,程序能够正常运行而不发生异常或错误的现象。换句话说,线程安全是指程序在多线程环境下,即使有多个线程并发执行,仍能保持数据的一致性和正确性。
重入(Reentrancy)是指一个方法或代码块在执行过程中可以被同一个线程再次调用,而不会导致冲突或不一致的情况。换句话说,重入是指当一个线程在执行一个方法时,如果该方法在执行过程中再次被相同线程调用,程序能正确处理这种情况,而不会引起死锁、资源冲突或数据错误。
结论:
申请一把锁是原子的,但申请两把锁就不是了,因临界资源需要多个锁才能进入,而不同的线程拥有不同的锁,导致两个或多个线程都在等待其它线程释放锁,造成死等待,进而造成死锁。
举个例子:一家棒棒糖超市老板有且仅有一个棒棒糖售价为1元,李四有0.5元,法外狂徒张三也有0.5元,因为棒棒糖为1元,所以两人都不可以买,李四在等张三把他的0.5元给我,法外狂徒张三也在等李四把他的0.5元给我,两人互不相让这就造成任何一个人都不能拿到棒棒糖,进而导致死锁。
如图所示:
直接使上述四个条件任意一个条件不成立即可避免死锁。
不是,因为STL容器是将性能挖掘到极致,一旦加锁保证线程安全,会对性能造成巨大影响,因此STL默认不是安全的,如果要保证安全,需要调用者自行保证线程安全。
本文介绍了线程池、单例模式、线程安全、死锁及STL与智能指针的线程安全性。线程池通过复用线程提升性能,适用于高并发和异步任务场景。单例模式确保类唯一实例,提供全局访问,分饿汉和懒汉两种实现。线程安全指多线程下数据一致性,重入是同一线程多次调用不冲突。死锁由互斥、请求保持、不剥夺、循环等待导致,需破坏任一条件避免。STL容器非线程安全,需自行加锁;智能指针中unique_ptr无此问题,shared_ptr通过原子操作保证引用计数安全。关于Linux系统部分的知识就已经全部更新完毕,下一步进入Linux网络部分,踏入新征程。