一个简陋线程池的实现

一个简陋线程池的实现

1 线程池的工作原理

线程池是什么?顾名思义,线程池就是一个放着若干线程的池子。整个线程池的工作原理是这样的,在一个池子里有若干个线程,此外还有任务队列,当任务队列为空的时候,线程池的所有线程也处于休眠(阻塞)状态;当任务队列中有任务的时候,需要唤醒线程来拿走任务队列中的任务;新任务产生的时候,这个任务会进入任务队列,等待线程来执行它。也就是说,我们需要维护两个数据结构,一个是线程池,另一个是任务队列。

2 为什么需要线程池?

线程池的优势就是,线程池的线程是一直存在的,也就是说我们不会像往常的做法那样,一旦有了一个任务就创建一个线程来执行它,执行完成后再把这个线程销毁,这样就节省了创建线程和销毁线程的开销, 因为虽然线程和进程相比,体量要小得多,但是反复地创建和销毁进程还是需要占用一定的资源。而线程池的出现,就允许线程在没有任务的时候阻塞,有任务的时候再被唤醒。

当然,线程池也不是所有场合都使用的。当任务产生的速度特别快的时候,如果线程池内线程的数量太少,就会使得任务的实时性大大降低。

3 怎么实现一个线程池?

这次的线程池是最简陋的实现,没有任务优先级(所有任务都是一样的优先级,遵循先来先执行),线程池大小在初始化之后不再变化,执行的任务都是void()类型。进行了这些简化以后,代码就变得很短。

3.1 代码和实现流程

//threadpool.h
#pragma once

#include <vector>
#include <functional>
#include <thread>
#include <queue>
#include <condition_variable>
#include <mutex>

using namespace std;

class ThreadPool
{
public:
	int thread_size;    //线程池大小(线程个数)

	typedef function<void()> Task;    //函数模板
	typedef deque<Task> Tasks;    
	//deque是类似于队列的数据结构,先进先出,而且在队尾添加元素和取出队首
	//元素的时间复杂度都是常数
	typedef vector<thread*> Threads;    //线程向量类

	ThreadPool(int size);
	~ThreadPool();

	void addTask(const Task&);

	void start();
	void stop();
	void threadLoop();
	Task take();

private:
	bool is_started;    //判断线程池是否开始工作
	Threads threads;    //线程向量,存放线程
	Tasks tasks;    //任务队列
	condition_variable cv;    //条件变量
	mutex t_mtx;    
		//任务队列的互斥锁,每个时刻最多只能有一个线程访问任务队列(要是
		//同时访问,比如同时添加任务,会出现并发问题)
};
//threadpool.cpp
#include "threadpool.h"

#include <iostream>
#include <assert.h>

using namespace std;

ThreadPool::ThreadPool(int size) : 
	is_started(false), 
	thread_size(size), 
	t_mtx(), 
	cv()
{
	start();    //线程池开始工作
}

ThreadPool::~ThreadPool()
{
	if (is_started)
	{
		stop();
	}
}

void ThreadPool::start()
{
	assert(threads.empty());    
		//线程池为空,才开始工作,不为空说明异常
		//(初始状态下线程池内没有线程)
	is_started = true;    //设置启动位
	for (int i = 0; i < thread_size; ++i)    //向线程池内添加线程
	{
		//thread t1(threadLoop);    //不能这么写
		//threads.push_back(t1);
		threads.push_back(new thread(bind(&ThreadPool::threadLoop, 
			this)));    //子线程执行threadLoop()
	}
}

void ThreadPool::threadLoop()    
	//每个线程都不断地尝试在任务队列中取任务
{
	while (is_started)
	{
		Task task = take();    //获取任务
		if (task)    //获取到任务
		{
			task();    //执行
		}
		//进入下一轮循环,再次尝试获取任务
	}
}

void ThreadPool::stop()
{
	is_started = false;    
		//标志位设为false,令子进程退出threadLoop()的循环
	cv.notify_all();    //会不会出现已经运行完的问题?

	for (int i = 0; i < threads.size(); ++i)
	{
		(threads[i])->join();    //等待子线程 *threads[i] 执行完
		delete threads[i];    //释放子线程指针指向的空间
	}
	threads.clear();
}

ThreadPool::Task ThreadPool::take()
{
	unique_lock<mutex> lck(t_mtx);    
		//这里的lck作用是锁住任务队列tasks,只有当前进程能访问tasks
		//因为要随时释放,所以最好用unique_lock而非lock_guard,
		//lock_guard在作用域结束的时候自动释放
	while (is_started && tasks.empty())
	{
		cv.wait(lck);    
			//没有任务,阻塞,释放锁lck,允许其他线程访问tasks,
			//addTask的时候唤醒
	}

	//该线程被唤醒
	Task task;
	int s = tasks.size();
	if (is_started && !tasks.empty())    //当前有任务
	{
		task = tasks.front();
		tasks.pop_front();
		assert(s - 1 == tasks.size());    
			//检查是否有别的线程访问并改动了tasks(同时加减可以吗?)
	}
	return task;
}

void ThreadPool::addTask(const Task& task)
{
	unique_lock<mutex> lck(t_mtx);    
		//锁住tasks,这个线程向tasks添加任务时别的线程不能访问tasks
		//可能用lock_guard也可以,开销更小
	tasks.push_back(task);    //添加任务
	cv.notify_one();    //释放锁lck,唤醒一个阻塞在take()的线程
}
#include "threadpool.h"

#include <iostream>

mutex out_mtx;    //输出的锁,防止多个线程同时输出互相干扰

void testFunc();

int main()
{
	ThreadPool thread_pool1(5);    //有5个线程的线程池
	for (int i = 0; i < 100; ++i)    //添加100个任务testFunc进入任务队列
	{
		thread_pool1.addTask(testFunc);
	}
	//这里要等加进去的任务全部执行完,因此还欠缺
	//system("pause");    //不能这么写,可能到这里时线程还没执行完
	getchar();    //等待所有任务执行完,输入任意字符,执行stop()
	thread_pool1.stop();
	system("pause");
	return 0;
}

void testFunc()
{
	unique_lock<mutex> lck(out_mtx);
	cout << "tid = " << this_thread::get_id() << endl;
}

以上就是线程池的实现。简单来说,就是主线程在线程池中放入若干个子线程,每个子线程都在threadLoop()不断循环,不断尝试take(),也就是在任务队列中拿任务出来执行。而take()做的事情就是,当任务队列为空的时候阻塞,直到有任务进入任务队列再去拿这个任务,然后就返回拿到的任务给上游的线程循环threadLoop(),threadLoop()就可以执行这个任务了。

这个流程是不难理解的,但是有些细节需要注意。

3.2 互斥锁的使用

这里用到了两个互斥锁,一个t_mux,还有一个out_mtx,它们一个是任务队列的锁,还有一个是输出的锁。什么意思呢?对于任务队列tasks来说,每个时刻只能有最多一个线程来访问它。 想象一下有两个线程同时向任务队列内增添任务,任务队列的最后一个任务是tail,那么它们可能最后会都在tail后面的一个位置增加这个任务(因为任务队列的大小还没来得及改动),最后的结果就是,只增加了一个任务,这显然不是我们想看到的。因此我们需要给任务队列加锁(不过假如有两个线程,有一个取走队首元素,有一个在队尾增添元素,这两个线程能同时执行吗? 不是很清楚)。

还有一个锁是输出锁,这个是很好理解的,假如有多个线程同时输出,那它们最后可能会同时输出在一行,而且可能是一个线程输出一部分,紧接着另一个线程又输出一部分,这样几个句子就杂糅在一起了。因此要给输出加锁。

3.3 一些C++语法

这次用到了函数模板function,并发编程中的条件变量condition_variable和互斥锁mutex,两种锁lock_guard和unique_lock(注意它们是一定义就自动上锁),注意这些C++的语法。

3.4 如何正确结束线程池的工作?

主线程需要等待子线程执行完再退出,留意stop()的代码,join()等子线程退出,delete释放子线程指针指向的空间。

2020.3.29

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值