简单的线程池
什么是线程池?
线程池就是线程的一种使用模式。虽然线程是轻量级的进程,但是线程的创建和销毁还是会引发效率问题。并且,如果创建的线程过多,反而会增加很多的调度开销,影响系统效率。
线程池就是可以提前创建好一些线程,在我们需要使用线程的时候,对已经创建好的线程添加任务就好。
线程池的使用场景:
-
需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
-
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
-
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
线程池原理:
这里我写的这个线程池的原理如下:
- 创建固定数量的线程,循环从任务队列(阻塞队列)中获取任务对象。
- 获取到任务对象后,执行任务对象中任务接口。
Code
阻塞队列:
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <unistd.h>
// 用信号量和环形数组来实现阻塞队列
// 信号量表示可用资源个数
// 一个信号量表示当前队列中元素的个数
// 另一个信号量表示当前队列中空格的个数
// 插入元素就是在消耗一个空格资源,释放了一个元素资源
// 删除元素就是在消耗一个元素资源,释放了一个空格资源
#include <semaphore.h>
template <class T>
class BlockingQueue
{
public:
BlockingQueue(int capacity)
:_head(0),
_tail(0),
_size(0),
_capicity(capacity)
{
_qu.resize(capacity);
sem_init(&_lock, 0, 1);
sem_init(&_elem, 0, 0);
sem_init(&_blank, 0, _capicity);
}
virtual ~BlockingQueue()
{
sem_destroy(&_lock);
sem_destroy(&_elem);
sem_destroy(&_blank);
}
void Push(const T& data)
{
// 每次插入元素前,申请一个空格资源
// 如果没有空格资源,说明队列满了
// 满了就不能继续插入,并且在 Push 中阻塞
sem_wait(&_blank);
sem_wait(&_lock);
_qu[_tail++] = data;
_tail %= _capicity;
_size++;
sem_post(&_lock);
sem_post(&_elem);
}
void Pop(T* data)
{
// 每次出队列前要申请一个元素资源
// 如果没有元素资源,队列为空
// 不能出队列,要在Pop 中阻塞
sem_wait(&_elem);
sem_wait(&_lock);
*data = _qu[_head++];
_head %= _capicity;
_size--;
sem_post(&_lock);
sem_post(&_blank);
}
private:
sem_t _lock;
std::vector<T> _qu;
int _head;
int _tail;
int _size;
int _capicity;
sem_t _elem;
sem_t _blank;
};
线程池
#pragma once
#include <iostream>
#include "BlockingQueue.hpp"
class Task
{
public:
virtual void Run()
{
std::cout << "Base Run" << std::endl;
}
virtual ~Task()
{
}
};
// 线程池对象启动的时候会创建一组线程
// 每个线程都会完成一定的任务(执行一定的代码逻辑, 这个逻辑是由调用者来决定)
//
// 任务是一段代码,就可以用函数来表示
class ThreadPool
{
public:
ThreadPool(int n)
:qu_(100),
woker_(n)
{
// 创建出若干个线程
for (int i = 0; i < n; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadEntry, this);
wokers_.push_back(tid);
}
}
~ThreadPool()
{
// 让线程退出,然后再回收
for (size_t i = 0; i < wokers_.size(); i++)
{
pthread_cancel(wokers_[i]);
}
for (size_t i = 0; i < wokers_.size(); i++)
{
pthread_join(wokers_[i], nullptr);
}
}
// 使用线程池的时候就需要由调用者加入一些任务
// 让线程去执行
void AddTask(Task *task)
{
qu_.Push(task);
}
private:
BlockingQueue<Task*> qu_;
int woker_;
std::vector<pthread_t> wokers_;
private:
static void *ThreadEntry(void *arg)
{
ThreadPool *pool = (ThreadPool *)arg;
while (true)
{
// 循环中尝试从阻塞队列中获取到一个任务
// 就执行一个任务
Task *task = nullptr;
pool->qu_.Pop(&task);
task->Run();
delete task;
}
return nullptr;
}
};
- 在创建对象的时候,会创建出若干线程,并且线程会从阻塞队列中获取任务。
- 这里有一个细节就是,如果阻塞队列为空,线程的创建也就会阻塞。所以在 Task 类中,Run函数可以定义为纯虚函数。
- 因为任务是 new 出来的,所以线程执行完任务需要将其 delete。
Main.cpp
#include "ThreadPool.hpp"
// 这个类由用户自己定制
// 需要依赖哪些数据,都可以随意添加和修改
class MyTask : public Task
{
public:
MyTask(int id)
:id_(id)
{
}
virtual void Run()
{
// 执行用户自定制的逻辑
std::cout << "id = " << id_ << std::endl;
}
private:
int id_;
};
int main()
{
ThreadPool pool(10);
for (int i = 0; i < 20; i++)
{
pool.AddTask(new MyTask(i));
}
while (1)
{
sleep(1);
}
return 0;
}
这是一个非常基础的线程池,结构也比较简单,也比较好懂。
叮~?