[Linux]——教你70行代码实现线程池

线程池

今天讲的这个玩应真的很有意思,他的名字叫线程池,看起来线程池像是一个贼高大上的东西,实际上实现一个简单的线程池还是挺简单的,如果你想顺利的自己学会实现一个简易的线程池,那么必须具备的技能有:熟悉队列操作,熟悉互斥锁,熟悉条件变量,当然有类封装的相关知识也是非常重要的。

线程池概念

话说什么池就放什么东西,水池放水,奖池放钱,那不言而喻线程池当然放的线程了,但是为什么要将线程放在一个池子中呢。我们来举一个生活中的例子吧,这样能帮你清楚的理解为什么需要线程池。

  • 你去楼下的餐馆吃饭,你点了一个西红柿炒鸡蛋,老板说:小伙子,让我现在去市场买鸡蛋,你心想:这也太墨迹了;老板到了市场买鸡蛋,但是卖鸡蛋的人说:师傅我家鸡正在下呢,你等会,老板心想:这也太墨迹了。所以又过了几天,你又去餐馆吃饭,你还是点了西红柿鸡蛋,这次老板变聪明了,早早的将鸡蛋备好,所以这次你吃的还算顺心。又过几天,你又去了点了西红柿鸡蛋,这次非常快一分钟就好了,你很疑惑,你问老板怎么这么快,老板说:大家上班都赶时间,我提前做好菜,你们来了就可以吃了。

简单的例子,相信不难理解为什么你吃饭一次比一次等的时间要短,因为老板使用了池化技术,早早将准备工作做好,所以效率大大提升,现在我们得出结论,线程池是为了解决效率问题而产生的。可是这也并不是线程池唯一的优点,现在我们将场景拉回到我们的软件开发中来。

线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量

现在进行总结,线程池保证了处理任务时的效率问题,因为接到任务既可以立马处理,并且对于短处理时间任务不用付出创建和销毁的代价。线程池的另一个好处就是保证了调度的合理性,如果没有上限的创建线程,一是导致调度周期变长,二是如果创建过度我们的程序就会直接挂掉。

线程池中始终维护着一定量的线程,有任务则处理,无任务他也始终在那,不发生任何状态的变化。
在这里插入图片描述

线程池应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了
  2. 性能要求苛刻的应用,比如要求服务器迅速响应客户请求
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误

其实线程池的应用场景就是围绕我们上面的优点来合理部署的,本篇博客因为是原生实现一个线程池,所以应用场景不能体现出来,后面笔者会写一个简易的tcp服务器接入线程池来给大家一个具体场景。

线程池实现

我们先来讨论一下线程池需要什么东西吧,封装线程池的类比不可少这个不用说,首先我们需要一个成员变量来限制线程池中最大的线程数。然后一个任务队列是必不可少的,这个队列中存放的是我们要处理的任务,而其他人向任务队列中放任务,这其实就是一个生产消费者模型,所以要保护队列(队列就是交易场所,他是临界资源)需要一把互斥锁。接下来我们设想如果线程池中很久没有任务,突然来了一个任务他肯定不能自动执行,所以我们需要一个条件变量。最后笔者加了一个闲置线程计数器,来记录闲置线程的个数,这个其实是可有可无的,不过既然写了就带上吧。

所以我们成员变量有:线程上限计算器,闲置线程计数器,互斥锁,条件变量,任务队列。

	class ThreadPool{
	private:
		int thread_nums;//线程池中的线程总数
		int idle_nums;//当前闲置线程的数量
		queue<Task> task_queue;//线程所要处理的任务队列
		pthread_mutex_t lock;
		pthread_cond_t cond;
	};

写一下构造和析构函数吧:缺省线程池的线程个数为5

	ThreadPool(int _num = 5) :thread_nums(_num), idle_nums(0)
	{
		pthread_mutex_init(&lock, NULL);//初始化锁和条件变量
		pthread_cond_init(&cond, NULL);
	}	
	~ThreadPool()
	{
		pthread_mutex_destroy(&lock);
		pthread_cond_destroy(&cond);
	}

接下来我们来创建线程池中的线程:

	void InitThreadPool()//创建所需要的线程
	{
		pthread_t t;
		for (auto i = 0; i < thread_nums; i++)
		{
			pthread_create(&t, NULL, ThreadRotinue, this);//让子线程执行我们的任务,这里传this我们下一个函数就说
		}
	}

创建线程后其实就是为了执行ThreadRotinue函数中的任务,那我们写一下这个函数:这个函数看似简单,可是这个函数实际上是毁灭此项目的灭霸,至少函数头中的static就够让人懵半天的了,不妨我们仔细刨析一下这个函数。下面有的函数被我封装了起来,先不用关心怎么实现,我会告诉大家功能,我们这里关心的重点并不在实现。

	static void* ThreadRotinue(void* arg)
	{
		pthread_detach(pthread_self());
		ThreadPool *tp = (ThreadPool*)arg;
		for (;;){
			tp->LockQueue();
			while (tp->QueueIsEmpty()){
				tp->ThreadIdle();//等待生产者生产
			}
			Task t;
			tp->PopTask(t);//从队列中拿出任务,其实就是先取队头数据,然后pop队头数据
			tp->UnlockQueue();
			t.Run();//执行任务
		}
	}
  • 问题1:函数头为什么要加static,你可不曾忘记pthread_create函数执行的回调函数必须是void* fun(void*)类型的,可是你不要忽略一个问题,这个函数我们最后写在类中,成员函数有个特性你没有忘记吧,那就是多传一个this指针,所以为了满足要求,我们不得不改成静态的,所以这也就是为什么创建线程时需要将this指针传给此函数。正如你所看到的,tp就是this指针,并且之后调用线程池中的其他函数都需要使用tp指针。
  • 问题2:等待生产者生产为什么需要使用while循环?if不也可以么?其实不然,如果等待函数被误唤醒或者等待失败,那么其实队列中现在并没有任务可以处理的任务,所以程序就会奔溃。
  • 问题3:为什么还要封装一个PopTask()函数?直接调用不就行了么?回到问题1,我们这个函数已经被转换为静态成员了,我们此时无法访问类的私有成员,所以必须另外封装一个接口
  • 问题4:t.Run()是让任务执行的调用,为什么不放在锁里执行,这样不会有线程安全问题么?这里真的非常十分爆炸重要,你可千万不要小看这一句代码,你可千万不要小看这一句代码,你可千万不要小看这一句代码,让我们思考一下,如果你已经得到了任务,还需要锁保护么?意思就是说,这个任务被你拿到就一定属于你了,以后不会再有任何人和你竞争了,这样放在锁外就没有任何问题了,但是这不是放在锁外的真正目的。如果你将这句代码放在锁中执行真是让人毛骨悚然,因为你在任务执行时拿着锁不释放,其他线程就会被阻塞住,这么一来你的程序居然变成了串行的了,天哪,本来我们就是为了效率而写的线程池,现在居然效率还不如之前,这不是笑话么,所以一定要擦亮眼睛看清这句代码的位置。

我们处理任务的函数有了,现在距离线程池核心部分完成就差最后一步了,向任务队列中push任务:

	void PushTask(const Task& t)
	{
		LockQueue();
		task_queue.push(t);
		WakeupThread();//唤醒线程
		UnlockQueue();
	}

这里解释一下为什么需要唤醒函数,如同上面说的,如果任务队列中很久没有任务,线程在处理任务函数中进行wait,那么这句代码就帮我们唤醒线程接着执行。现在我们线程池的核心部分全部完成,最后我们将上面使用到的封装函数完成这个线程池就完成了:

下面这些都是很容易理解的函数,我们把提到的函数全部塞到线程池类中我们的线程池就完成了。

	void LockQueue()
	{
		pthread_mutex_lock(&lock);
	}
	void UnlockQueue()
	{
		pthread_mutex_unlock(&lock);
	}
	bool QueueIsEmpty()
	{
		return task_queue.size() == 0 ? true : false;
	}
	void ThreadIdle()
	{
		idle_nums++;//一旦进入等待状态闲置线程加一
		pthread_cond_wait(&cond, &lock);
		idle_nums--;
	}
	void WakeupThread()
	{
		pthread_cond_signal(&cond);
	}
	void PopTask(Task& t)
	{
		t = task_queue.front();
		task_queue.pop();
	}

我们为了让大家更好的理解线程池,这里笔者模拟一次线程池的使用,让大家加深记忆,最后我会把所有的代码打包放在最后。

使用线程池

使用线程池就很简单,我们这里定义一个任务类,表示我们需要完成什么样的任务,我这里写了一个最简单的计算器,所以任务中需要定义关于计算器的一切东西。

typedef int(*HandlerTask_t) (int x, int y, int op);
class Task{
private:
	int x;//用户要处理的数据和方式
	int y;
	int op;//0+ 1- 2* 3/
	HandlerTask_t handler;
public:
	Task(int _x = -1, int _y = -1, int _op = -1) :x(_x), y(_y), op(_op){}
	void Register(HandlerTask_t _handler)
	{
		handler = _handler;//注册你要处理的方式
	}
	void Run()
	{
		int ret = handler(x, y, op);//handler函数留给用户自己实现
		const char* arr = "+-*/";
		cout << pthread_self() << " : " << x << arr[op] << y << "=" << ret << endl;
	}
	~Task(){}
};

任务这个大家可以自己随意定义,这里笔者这个任务仅供参考,只要有一个方法处理你的任务就好,接下来我们完成用户所要完成的事:

int cal(int x, int y, int op)//用户自己定义需要的处理过程
{
	int ret = -1;
	switch (op){
	case 0:
		ret = x + y;
		break;
	case 1:
		ret = x - y;
		break;
	case 2:
		ret = x * y;
		break;
	case 3:
		ret = x / y;
		break;
	default:
		std::cout << "cal error!" << std::endl;
	}
	return ret;
}
int main()
{
	ThreadPool tp;
	tp.InitThreadPool();//初始化线程池
	srand((unsigned long)time(NULL));
	for (;;){
		int x = rand() % 100 + 1;
		int y = rand() % 100 + 1;
		int op = rand() % 4;
		Task t(x, y, op);//创建任务
		t.Register(cal);//注册处理方式
		tp.PushTask(t);//向队列中push任务
		sleep(1);
	}
	return 0;
}

来看看我们的执行结果:大功告成!!!!
在这里插入图片描述
使用ps -ajL命令查找你的线程池任务:
在这里插入图片描述

总结

实现一个线程池其实并不难,但是也绝对不简单,实现中有很多小细节,包括有些代码的位置,或者某些函数为什么是静态的。读者一定要深刻明白ThreadRotinue函数中的4个问题,这是实现线程池的核心函数。后面我们给大家在tcp简易服务器中实现线程池接入,有兴趣的老铁还请多关注。

附件

所有实现代码给大佬们奉上:
在这里插入图片描述

#include<iostream>
#include<pthread.h>
#include<time.h>
#include<unistd.h>
#include<stdlib.h>
#include<queue>
using namespace std;

int cal(int x, int y, int op);
typedef int(*HandlerTask_t) (int x, int y, int op);
class Task{
private:
	int x;
	int y;
	int op;//0+ 1- 2* 3/
	HandlerTask_t handler;
public:
	Task(int _x = -1, int _y = -1, int _op = -1) :x(_x), y(_y), op(_op){}
	void Register(HandlerTask_t _handler)
	{
		handler = _handler;
	}
	void Run()
	{
		int ret = handler(x, y, op);
		const char* arr = "+-*/";
		cout << pthread_self() << " : " << x << arr[op] << y << "=" << ret << endl;
	}
	~Task(){}
};

class ThreadPool{
private:
	int thread_nums;//线程池中的线程总数
	int idle_nums;//当前闲置线程的数量
	queue<Task> task_queue;//线程所要处理的任务队列
	pthread_mutex_t lock;
	pthread_cond_t cond;
public:
	void LockQueue()
	{
		pthread_mutex_lock(&lock);
	}
	void UnlockQueue()
	{
		pthread_mutex_unlock(&lock);
	}
	bool QueueIsEmpty()
	{
		return task_queue.size() == 0 ? true : false;
	}
	void ThreadIdle()
	{
		idle_nums++;//一旦进入等待状态闲置线程加一
		pthread_cond_wait(&cond, &lock);
		idle_nums--;
	}
	void WakeupThread()
	{
		pthread_cond_signal(&cond);
	}
	void PopTask(Task& t)
	{
		t = task_queue.front();
		task_queue.pop();
	}
public:
	ThreadPool(int _num = 5) :thread_nums(_num), idle_nums(0)
	{
		pthread_mutex_init(&lock, NULL);
		pthread_cond_init(&cond, NULL);
	}
	static void* ThreadRotinue(void* arg)
	{
		pthread_detach(pthread_self());
		ThreadPool *tp = (ThreadPool*)arg;
		for (;;){
			tp->LockQueue();
			while (tp->QueueIsEmpty()){
				tp->ThreadIdle();
			}
			Task t;
			tp->PopTask(t);
			tp->UnlockQueue();
			t.Run();
		}
	}
	void InitThreadPool()//创建所需要的线程
	{
		pthread_t t;
		for (auto i = 0; i < thread_nums; i++)
		{
			pthread_create(&t, NULL, ThreadRotinue, this);
		}
	}
	void PushTask(const Task& t)
	{
		LockQueue();
		task_queue.push(t);
		WakeupThread();
		UnlockQueue();
	}
	~ThreadPool()
	{
		pthread_mutex_destroy(&lock);
		pthread_cond_destroy(&cond);
	}
};

int cal(int x, int y, int op)
{
	int ret = -1;
	switch (op){
	case 0:
		ret = x + y;
		break;
	case 1:
		ret = x - y;
		break;
	case 2:
		ret = x * y;
		break;
	case 3:
		ret = x / y;
		break;
	default:
		std::cout << "cal error!" << std::endl;
	}
	return ret;
}
int main()
{
	ThreadPool tp;
	tp.InitThreadPool();
	srand((unsigned long)time(NULL));
	for (;;){
		int x = rand() % 100 + 1;
		int y = rand() % 100 + 1;
		int op = rand() % 4;
		Task t(x, y, op);
		t.Register(cal);
		tp.PushTask(t);
		sleep(1);
	}
	return 0;
}
  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值