[linux]-------线程池

上一篇博客我们实现了读者写者模型,在这篇博客中更进一步的完成线程池。

线程池概念

开辟一块内存空间,里面存在大量的(未死亡的)线程,池中的线程调度由池管理器来处理,当有线程任务时,从池中选一个线程运行,当运行完毕后,该线程又返回池中,这样就避免了反复创建线程所带来的性能开销,节省了系统资源。

如果对线程概念不清晰的话,不妨先看看我是一个线程这篇文章

线程池的应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短,例如WEB服务器完成网页请求。
  2. 对性能要求苛刻的应用,比如要求服务器立即响应客户请求
  3. 接受突发性的大量请求,但是不至于时服务器因此产生大量的线程的应用。
    线程池就是围绕上面几种情况而产生的,下面我们自己实现一个线程池,暂时无法体现出线程池的应用场景。

线程池的实现

我们需要设计线程池这个类的成员有哪些。

  1. 计数器 用于记录空余线程的个数
  2. 任务队列:用于储存任务的缓冲区
  3. 信号量
class thread_pool
{
private:
	size_t thread_num;//记录当前有多少进程空闲
	queue<Task> thread_queue;
	pthread_cond_t cond;
	pthread_mutex_t mutex;
}

构造函数和析构函数主要是对信号量和锁进行初始化

thread_pool(int num = 5): thread_num(num)
	{
		pthread_cond_init(&cond,nullptr);
		pthread_mutex_init(&mutex, nullptr);
	}
	~thread_pool()
	{
		pthread_cond_destroy(&cond);
		pthread_mutex_destroy(&mutex);
	}

创建线程,对线程池初始化

	void Init_thread_pool()
	{
		pthread_t id;
		for(int i = 0; i < thread_num; i++)
		{
			pthread_create(&id, nullptr, Run_func, this);//注意把this作为变量传给Run_func,这里只做演示只写了一个运行的函数。
		}
	}

下面是整个线程池的重点,执行任务。

static void* Run_func(void* arg)
	{
		pthread_detach(pthread_self());
		thread_pool* pool = (thread_pool*) arg;
		while (1)
		{
			pool->thread_queue;
			while (pool->Is_empty())
			{
				pool->thread_Idle();//idle
			}
			Task it;
			pool->pop_task(it);
			pool->unlock_queue();
			it.Run();
		}

首先,因为pthread_create函数的第三个参数为 void *(*start_routine) (void *),但是对于类中的非静态函数,已经隐含的传入了this变量,因此我们必须把该函数设为静态变量,并且把this指针作为参数传入函数中。
其次,可以发现使用while循环而不是用if判断队列是否为空,这是因为有多个线程可能会同时操作,while循环能够防止误判,下面这一段虽然说的是生产者消费者模式的wait,但是我觉得也能说明问题

永远不要在循环之外调用wait方法
《Effective Java》第二版中文版第69条244页位置对这一点说了一页,生产者和消费者问题来说:错误情况一:如果有两个生产者A和B,一个消费者C。当存储空间满了之后,生产者A和B都被wait,进入等待唤醒队列。当消费者C取走了一个数据后,如果调用了notifyAll(),注意,此处是调用notifyAll(),则生产者线程A和B都将被唤醒,如果此时A和B中的wait不在while循环中而是在if中,则A和B就不会再次判断是否符合执行条件,都将直接执行wait()之后的程序,那么如果A放入了一个数据至存储空间,则此时存储空间已经满了;但是B还是会继续往存储空间里放数据,错误便产生了。错误情况二:如果有两个生产者A和B,一个消费者C。当存储空间满了之后,生产者A和B都被wait,进入等待唤醒队列。当消费者C取走了一个数据后,如果调用了notify(),则A和B中的一个将被唤醒,假设A被唤醒,则A向存储空间放入了一个数据,至此空间就满了。A执行了notify()之后,如果唤醒了B,那么B不会再次判断是否符合执行条件,将直接执行wait()之后的程序,这样就导致向已经满了数据存储区中再次放入数据。错误产生。

最后我们发现,任务的执行是在锁外面而不是在锁里面,难道不管进程安全了吗?,仔细想想,发现当得到了分配的线程就没人和你竞争了,既然没人和你竞争了,自然也不需要锁的保护了,另外假如我们让任务的执行放到锁里面,事实上,是让程序变成了串行,效率还不如之前。

完成只差一步,下面的函数,用来传入任务,唤醒函数是为了防止队列里原来没有任务,插入新的任务需要唤醒。

void push_task(const Task& t)
	{
		lock_queue();
		thread_queue.push(t);
		wake_thread();
		unlock_queue();
	}

完成了上面的函数,基本上就把线程池完成了大半,下面是完整的代码。

#include <queue>
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
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 _left;
	int _right;
	int _op;
	HandlerTask_t _handler;
public:
	Task(int left = 0, int right = 0, int op = 0) :_left(left), _right(right), _op(op)
	{
	}

	void Register(HandlerTask_t handler)
	{
		_handler = handler;
	}

	void Run()//任务执行
	{
		int ret = _handler(_left, _right, _op);
		const char* str = "+-*/";
		cout << "thread is [" << pthread_self() << _left << str[_op] << _right << "=" << ret << endl;

	}
	~Task()
	{

	}
};
class thread_pool
{
private:
	size_t thread_num;//记录当前有多少进程空闲
	queue<Task> thread_queue;
	pthread_cond_t cond;
	pthread_mutex_t mutex;
public:
	thread_pool(int num = 5): thread_num(num)
	{
		pthread_cond_init(&cond,nullptr);
		pthread_mutex_init(&mutex, nullptr);
	}
	~thread_pool()
	{
		pthread_cond_destroy(&cond);
		pthread_mutex_destroy(&mutex);
	}

	void Init_thread_pool()
	{
		pthread_t id;
		for(int i = 0; i < thread_num; i++)
		{
			pthread_create(&id, nullptr, Run_func, this);//注意把this作为变量传给Run_func,这里只做演示只写了一个运行的函数。
		}
	}

	void lock_queue()
	{
		pthread_mutex_lock(&mutex);
	}

	void unlock_queue()
	{
		pthread_mutex_unlock(&mutex);
	}

	bool Is_empty()
	{
		return thread_queue.size() == 0;
	}

	void thread_Idle()
	{
		thread_num++;
		pthread_cond_wait(&cond, &mutex);
		thread_num--;
	}
	void wake_thread()
	{
		pthread_cond_signal(&cond);
	}

	void pop_task(Task& t)
	{
		t = thread_queue.front();
		thread_queue.pop();
	}
	
	void push_task(const Task& t)
	{
		lock_queue();
		thread_queue.push(t);
		wake_thread();
		unlock_queue();
	}
		static void* Run_func(void* arg)
	{
		pthread_detach(pthread_self());
		thread_pool* pool = (thread_pool*) arg;
		while (1)
		{
			pool->thread_queue;
			while (pool->Is_empty())
			{
				pool->thread_Idle();//idle
			}
			Task it;
			pool->pop_task(it);
			pool->unlock_queue();
			it.Run();
		}


	}

};



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:
		cout << "cal error!" << endl;
		break;
	}
}

int main()
{
	thread_pool tp;
	tp.Init_thread_pool();
	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.push_task(t);
		sleep(1);
	}
	return 0;
}

运行结果如下(仔细想想是真的闲)
在这里插入图片描述

总结

线程池是为了需要大量的线程来完成任务,且完成任务的时间比较短的情况而产生的,节省了开辟销毁线程的资源开销,可以使用多种方式实现.

完成了上面的代码,我们对于linux的线程的学习告一段落,在下篇博客中我们终于可以学习网络相关的知识。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章