POSIX下简单线程池的实现

POSIX下简单线程池的实现

什么是线程池?为什么要有线程池?

进程是资源分配的基本单位
线程是能独立运行的基本单位
一个进程内的线程共享资源
因此,线程的创建开销小于进程——不需要执行进程复制、分配页表等操作
线程的同步开销小于进程——不需要进程间通信,因为线程之间能共享资源

在客户机-服务器模型中,每当添加一个新的连接时,就需要创建一个新的线程进行响应。当连接结束后,需要释放当前的线程。
线程的开销虽然小,但是并不是意味着没有开销

并且,更加严重的问题是,如果同时有大量连接产生,那么,分配的线程的数量很容易就会超出操作系统的限制

这些问题的解决方法就是使用线程池
它的主要思想是:

  1. 进程开始时创建一定数量的线程放入池中
  2. 服务器接收请求后,唤醒一个线程,并将要处理的请求传递给它
  3. 线程完成服务后,会返回池中等待工作
  4. 若池中无可用线程,则服务器会一直等待到有空线程为止

这么做的优点是:

  1. 不需要频繁地创建、销毁线程
  2. 限制了可以使用的线程的数量

实现

第一步,设计数据结构

线程池应当有一个具体的大小,将其设置为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);

第三步,具体实现

首先实现初始化线程池的接口
它的任务有:

  1. 分配thread_id数组空间
  2. 初始化条件变量和互斥锁
  3. 建立线程
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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值