POSIX下简单线程池的实现
什么是线程池?为什么要有线程池?
进程是资源分配的基本单位
线程是能独立运行的基本单位
一个进程内的线程共享资源
因此,线程的创建开销小于进程——不需要执行进程复制、分配页表等操作
线程的同步开销小于进程——不需要进程间通信,因为线程之间能共享资源
在客户机-服务器模型中,每当添加一个新的连接时,就需要创建一个新的线程进行响应。当连接结束后,需要释放当前的线程。
线程的开销虽然小,但是并不是意味着没有开销
并且,更加严重的问题是,如果同时有大量连接产生,那么,分配的线程的数量很容易就会超出操作系统的限制
这些问题的解决方法就是使用线程池
它的主要思想是:
- 进程开始时创建一定数量的线程放入池中
- 服务器接收请求后,唤醒一个线程,并将要处理的请求传递给它
- 线程完成服务后,会返回池中等待工作
- 若池中无可用线程,则服务器会一直等待到有空线程为止
这么做的优点是:
- 不需要频繁地创建、销毁线程
- 限制了可以使用的线程的数量
实现
第一步,设计数据结构
线程池应当有一个具体的大小,将其设置为max_thread_num
知道了线程池的大小后,我们应当开始创建线程,并将创建了的线程的thread_id
存储起来,因此,我们需要一个数组,记作tid
当有新的任务加入时,若线程池处于已满状态——即所有线程都在工作中,此时,应当暂时存储这些任务,并等待有新的线程空闲后再加入。任务的启动顺序应当和加入顺序一致。因此,使用队列进行存储。
新的任务需要的参数有两个:一是任务启动入口,二是任务参数
任务启动入口,即一个函数指针
设计队列结构如下:
struct tpool_task {
void *(*routine)(void *); /* the entry of new thread */
void *args; /* args of new thread */
struct tpool_task *next;
};
由于同时有多个线程在对这个队列不断地进行存、取操作,因此,有必要设置一个互斥锁将其锁起来
当一个线程处于空闲状态的时候,它应当被挂起等待,直到任务队列有新的任务加入为止。但是,对任务队列进行操作又需要对其加锁,这就导致了同一个时刻只可能有一个线程,即最早空闲的线程,获得任务队列的控制权。此时,新增任务到任务队列的行为便变得不可能了——因为新增队列的线程和当前线程不是同一个线程。这将导致死锁——获得锁的线程等待新增任务,而能新增任务的线程等待锁的释放。
实际上,互斥锁只能表达“线程能否访问被保护的资源”这一个信息,它并不能表达“被保护的资源是否足够”这个信息。
https://www.cnblogs.com/sinkinben/p/14087320.html
这是一个简单的生产者-消费者问题
https://baike.baidu.com/item/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85%E9%97%AE%E9%A2%98/8412057
生产者是往队列里面添加任务的线程
消费者是从队列里面取出任务的线程
互斥锁只能告诉生产者“现在你可以往里面放东西”,或者告诉消费者“现在你可以从里面拿东西”,却不能告诉生产者“你能往里面放多少问题”或者告诉消费者“里面是不是有东西让你拿”
生产者-消费者问题有两个模型,一个是无限缓存模型,即队列无限长;另一个是有限缓存模型,即队列长度有限。我们的问题属于无限缓存模型。显然,不存在生产者不能放东西的情景,只存在消费者没有东西可以拿的情景。
要解决这个问题,需要的思路很简单:如果消费者发现缓存是空的,那么,它就释放锁,等待缓存非空后再获取锁
获取锁
if (缓存为空) {
释放锁
等待缓存非空
获得锁
}
操作
释放锁
POSIX中有一个称之为条件变量的东西,就是上述过程的封装
条件变量配合互斥锁,能实现原子操作
因此,除了互斥锁外,我们再创建一个条件变量
除了以上的信息外,我们还需要知道,线程池什么时候需要被销毁(或者说,其他的线程需要知道自己什么时候应当退出),因此,设置一个shutdown
变量进行指示
现在,我们的数据结构如下:
struct tpool {
size_t max_thread_num; /* the max num of threads */
pthread_t *tid; /* an array of threads */
pthread_mutex_t queue_lock;
pthread_cond_t queue_ready; /* to ensure atomic operation */
struct tpool_task *queue_head; /* a queue of tasks */
size_t shutdown; /* is pool shut down */
};
第二步,我们需要设计线程池对外的接口
因为是简单线程池,所以不需要有太多功能,我们设计如下接口:
int tpool_init(struct tpool *pool, size_t max_thread_num);
void tpool_destroy(struct tpool *pool);
int tpool_add_task(struct tpool *pool, void *(*routine)(void *), void *args);
第三步,具体实现
首先实现初始化线程池的接口
它的任务有:
- 分配
thread_id
数组空间 - 初始化条件变量和互斥锁
- 建立线程
int tpool_init(struct tpool *pool, size_t max_thread_num)
{
pool->max_thread_num = max_thread_num;
pool->tid = (pthread_t *) malloc(max_thread_num * sizeof(pthread_t));
pool->queue_head = NULL;
pool->shutdown = 0;
/* malloc failed or init failed */
if (NULL == pool->tid ||
pthread_mutex_init(&pool->queue_lock, NULL) != 0 ||
pthread_cond_init(&pool->queue_ready, NULL) != 0)
return -1;
/*create threads */
for (int i = 0; i < max_thread_num; i++) {
if (pthread_create(&pool->tid[i], NULL,
task_routine, (void *)pool) != 0)
return -1; /* thread created failed */
}
return 0