Linux基础组件之线程池的实现

线程池存在原因

线程使用场景

某类任务特别耗时,会严重影响该线程处理其他任务,因此需要在其他线程异步执行该任务。

main thread
......
任务A
任务B
create thread
任务D
new thread
销毁线程
任务C

线程开销

随着这类任务越来越多,需要异步执行任务而开启的线程也越来越多,但是每个CPU的核心数和线程数是固定,过多的线程并不能提高效率。因此,线程资源的开销与CPU核心之间要平衡选择。

原因

线程资源的开销与CPU核心之间要平衡选择,自然就产生线程池,即需要固定线程的数量。
从前面的执行流程可以看出,每次耗时任务到来都需要不断的创建线程,执行完成后销毁线程。不断的创建和销毁线程会浪费系统资源。
因此需要有线程池,提前分配固定线程数量,不会将它们关闭,当任务到来时复用这些线程。

main thread
......
任务A
任务B
run thread
任务D
thread pool
任务C
initiation thread pool

作用

(1)复用线程资源。
(2)充分利用系统资源。
(3)异步执行耗时任务。
(4)减少了多个任务(不是一个任务)的执行时间

线程池

线程池是一个生产–消费模型。发布任务的线程是生产者,其他线程是消费者。

构成

(1)生产者线程:发布任务。
(2)队列:亦称任务队列,存放具体的任务。因为任务是异步执行的,任务的内容就包括了任务的上下文以及任务的执行函数。
(3)线程池:即消费者,是固定数量的线程集合;主要完成取出任务、执行任务、任务调度。

线程调度

由于任务的密疏程度是未知的,即任务是间歇性的,有时候任务很多,有时候任务很少。当任务很少时,需要将不执行任务的线程休眠,不能让其浪费系统资源。这就需要线程调度。

线程的调度主要通过mutex和condition实现。 即互斥锁和条件变量。
线程有两种状态:从无任务到有任务(从无到有)以及从有任务到无任务(从有到无)。

利用条件变量,从无到有时,唤醒线程;从有到无时,休眠线程。那么如何确定条件呢?就是依据任务队列的状态,如果任务队列中有任务,将线程唤醒;如果任务队列为空,将线程休眠。

仔细点说,就是:
当生产者线程发布任务时,任务队列就有任务,进入 从无到有状态;通知某一个线程唤醒,取出任务、执行任务。
线程判断任务队列是否有任务,如果任务队列为空,则进入 从有到无 状态,condition通知线程休眠。

平衡选择

线程资源的开销与CPU核心之间做平衡选择;平衡选择依据耗时任务而定。耗时任务分为IO密集型和CPU密集型。

IO密集型:IO的操作是同步的,系统调用会阻塞的将内核资源拷贝到用户态或者用户态资源阻塞的将资源拷贝到内核中;线程会阻塞等待系统调用完成。
CPU密集型:长时间占用CPU,使线程无法处理其他任务。

根据这两个类型,可以确定线程池的线程数量。一般,CPU密集型的线程池数量等于CPU核心数;IO密集型的线程池线程数量等于2倍核心数+2。
有这样一个公式: (IO等待时间+CPU运算时间)核心数/cpu运算时间。根据公式对线程池数量做优化调整,使其符合特定业务逻辑。

实现一个线程池

接口设计

(1)创建线程池的接口。确定线程池的线程数量以及任务队列的长度。
(2)销毁线程池的接口。线程判断线程池销毁标志,如果标记了线程池销毁,线程退出;并且通知所有线程。
(3)生产者线程抛出任务的接口。目的是构造一个任务,并把任务放到任务队列中,通知线程唤醒。

Thread pool
创建线程池
接口设计
销毁线程池
抛出任务
线程池线程数量
队列长度
标记线程池退出
通知所有线程
构造任务
放入队列
通知线程唤醒

代码示例

thread_pool.h

#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H

typedef struct thread_pool_t thread_pool_t;
typedef void(*handler_pt)(void*);

enum THREAD_POOL_ERROR_CODE
{
	THREAD_POOL_NULL = -1,
	THREAD_MUTEX_FAIL = -2,
	THREAD_POOL_CLOSED = -3,
	THREAD_COND_FAIL = -4,
	THREAD_POOL_TASK_QUE_FULL = -5,
	THREAD_COND_SIGNAL_FAIL=-6,
	THREAD_POOL_SUCCESS = 0
};

thread_pool_t *thread_pool_create(int thread_count,int queue_size);

int thread_pool_destory(thread_pool_t *pool);

int thread_pool_post(thread_pool_t *pool,handler_pt func,void* arg);

int wait_pool_done(thread_pool_t *pool);

#endif

thread_pool.c

#include <pthread.h>
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include "thread_pool.h"


typedef struct task_t{
	handler_pt func;
	void *arg;
} task_t;


typedef struct task_queue_t{
	uint32_t head;// 队列头索引
	uint32_t tail;// 队列尾索引
	uint32_t count;//任务数量
	task_t *queue;//队列数组
} task_queue_t;

struct thread_pool_t{
	pthread_mutex_t mutex;
	pthread_cond_t condition;
	pthread_t *threads;
	task_queue_t task_queue;
	
	int closed;//销毁线程池标记
	int started;//当前运行的线程数
	
	int thread_count;
	int queue_size;
	
	
};

static void thread_pool_free(thread_pool_t *pool)
{
	if (pool == NULL || pool->started > 0)
		return;

	if (pool->threads)
	{
		free(pool->threads);
		pool->threads = NULL;

		pthread_mutex_lock(&(pool->mutex));
		pthread_mutex_destroy(&(pool->mutex));
		pthread_cond_destroy(&(pool->condition));
	}

	if (pool->task_queue.queue)
	{
		free(pool->task_queue.queue);
		pool->task_queue.queue = NULL;
	}
	free(pool);
}

static void *thread_worker(void *thread_pool)
{
	thread_pool_t *pool = thread_pool;
	task_queue_t *que;
	task_t task;
	for (;;)
	{
		pthread_mutex_lock(&(pool->mutex));//加锁
		que = &(pool->task_queue);
		// 判断虚假唤醒
		while (que->count == 0 && pool->closed == 0)
		{
			// pthread_mutex_unlock(&(pool->mutex))
			// 阻塞在 condition
			// 唤醒信号===================================
			// 解除阻塞
			// pthread_mutex_lock(&(pool->mutex));
			pthread_cond_wait(&(pool->condition),&(pool->mutex));
		}

		if (pool->closed == 1)
			break;

		task = que->queue[que->head];//取出任务
		que->head = (que->head + 1) % pool->queue_size;
		que->count--;
		pthread_mutex_unlock(&(pool->mutex));
		(*(task.func))(task.arg);
	}
	pool->started--;
	pthread_mutex_unlock(&(pool->mutex));
	pthread_exit(NULL);
	return NULL;
}

thread_pool_t *thread_pool_create(int thread_count,int queue_size)
{
	if(thread_count<=0 || queue_size<=0)
		return NULL;
	thread_pool_t *pool;
	pool=(thread_pool_t *)malloc(sizeof(*pool));

	if(pool==NULL)
		return NULL;
	
	pool->thread_count = 0;//从0开始计数
	pool->queue_size = queue_size;
	pool->task_queue.head = 0;
	pool->task_queue.tail = 0;
	pool->task_queue.count = 0;
	pool->started = pool->closed = 0;

	pool->threads = NULL;
	pool->task_queue.queue = NULL;

	pool->task_queue.queue = (task_t*)malloc(sizeof(task_t)*queue_size);
	if (pool->task_queue.queue == NULL)
	{
		//free pool
		thread_pool_free(pool);
		return NULL;
	}
		
	pool->threads = (pthread_t*)malloc(sizeof(pthread_t)* thread_count);
	if (pool->threads == NULL)
	{
		//free pool
		thread_pool_free(pool);
		return NULL;
	}

	int i = 0;
	for (i = 0; i < thread_count; i++)
	{
		if (pthread_create(&(pool->threads[i]), NULL,thread_worker, (void*)pool) != 0)
		{
			//free pool
			thread_pool_free(pool);
			return NULL;
		}
		pool->thread_count++;
		pool->started++;
	}
	return pool;
}

int thread_pool_post(thread_pool_t *pool, handler_pt func, void* arg)
{
	if (pool == NULL || func == NULL)
		return THREAD_POOL_NULL;

	task_queue_t *task_queue = &(pool->task_queue);//取出队列

	if (pthread_mutex_lock(&(pool->mutex)) != 0)
		return THREAD_MUTEX_FAIL;

	if (pool->closed)
	{
		pthread_mutex_unlock(&(pool->mutex));
		return THREAD_POOL_CLOSED;
	}

	if (task_queue->count==pool->queue_size)
	{
		pthread_mutex_unlock(&(pool->mutex));
		return THREAD_POOL_TASK_QUE_FULL;
	}

	task_queue->queue[task_queue->tail].func = func;
	task_queue->queue[task_queue->tail].arg = arg;
	task_queue->tail = (task_queue->tail + 1) % pool->queue_size;
	task_queue->count++;

	if (pthread_cond_signal(&(pool->condition)) != 0)
	{
		pthread_mutex_unlock(&(pool->mutex));
		return THREAD_COND_SIGNAL_FAIL;
	}

	pthread_mutex_unlock(&(pool->mutex));
	return 0;
}

int wait_pool_done(thread_pool_t *pool)
{
	int i, ret = 0;
	for (i = 0; i < pool->thread_count; i++)
	{
		if (pthread_join(pool->threads[i], NULL) != 0)
			ret = 1;
	}
	return ret;
}

int thread_pool_destory(thread_pool_t *pool)
{
	if (pool == NULL)
		return THREAD_POOL_NULL;

	if (pthread_mutex_unlock(&(pool->mutex)) != 0)
		return THREAD_MUTEX_FAIL;

	if (pool->closed)
	{
		thread_pool_free(pool);
		return THREAD_POOL_CLOSED;
	}
	pool->closed = 1;

	if(pthread_cond_broadcast(&(pool->condition))!=0 ||
		pthread_mutex_unlock(&(pool->mutex)) != 0)
	{
		thread_pool_free(pool);
		return THREAD_COND_FAIL;
	}

	wait_pool_done(pool);
	thread_pool_free(pool);
	return 0;
}

使用线程池,main.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include "thread_pool.h"

int num = 0;
int done = 0;
pthread_mutex_t lock;

void do_task(void *arg)
{
	usleep(3000);
	pthread_mutex_lock(&lock);
	done++;
	printf("doing %d task\n", done);
	pthread_mutex_unlock(&lock);
}

int main(int argc,char **argv)
{
	int threads = 4;
	int queue_size = 256;

	if (argc == 2)
	{
		queue_size=threads = atoi(argv[1]);
		if (threads <= 0 || queue_size <= 0)
		{
			printf("threads number or queue size error: %d,%d\n", threads, queue_size);
			return 1;
		}
	}

	thread_pool_t *pool = thread_pool_create(threads, queue_size);
	if (pool == NULL)
		return THREAD_POOL_NULL;

	while (thread_pool_post(pool, &do_task, NULL) == 0)
	{
		pthread_mutex_lock(&lock);
		num++;
		pthread_mutex_unlock(&lock);
	}

	printf("add %d tasks\n", num);

	wait_pool_done(pool);
	printf("did %d tasks\n", done);
	thread_pool_destory(pool);
	return 0;
}

开源项目的线程池

nginx中线程池

nginx中线程池的作用是处理文件缓冲。 nginx线程池默认关闭,configure 时,需要 --with-threads
来指定。
线程池作用阶段:

read
Decode
Compute
Encode
Write
thread pool

(1)使用线程池的情况:nginx可以应用于静态web服务器,主要是处理文件缓冲,文件读写比较耗时。nginx推荐使用sendfile、directio、aio来处理耗时的任务,线程池不是重点推荐。nginx的耗时主要集中在文件操作,即compute阶段,这时可以使用线程池解决。
(2)使用线程池原因:磁盘IO读写比较耗时。nginx推荐使用sendfile、directio、aio来处理耗时的任务,线程池不是重点推荐。
(3)使用线程池:nginx线程池会有两个队列,任务消息队列和完成消息队列;任务消息队列存放发布的任务,将任务pull到线程池;线程池处理完会将结果push到完成消息队列,通知主线程获取结果。

TOP
push
notify
pull
push
thread pool
thread#1
thread#2
thread#...
主线程
任务消息队列
完成消息队列

niginx github开源位置

ngx_thread_pool.c
ngx_http_file_cache.c
调用
ngx_thread_pool_init
ngx_thread_pool_destroy
ngx_thread_task_post函数
ngx_thread_mutex_lock函数
ngx_thread_cond_signal函数
ngx_thread_mutex_unlock函数
ngx_http_cache_thread_handler函数

ngx_http_file_cache.c

static ngx_int_t
ngx_http_cache_thread_handler(ngx_thread_task_t *task, ngx_file_t *file)
{
	// 其他代码...
	task->event.data = r;
    task->event.handler = ngx_http_cache_thread_event_handler;
    
	if (ngx_thread_task_post(tp, task) != NGX_OK) {
        return NGX_ERROR;
    }
    // 其他代码...
}

redis中线程池

redis是作为一个数据库,需要读写大量的数据、解析协议,这样一来读IO和写IO压力都非常大。因此,redis中线程池的作用是读写IO处理和数据包解析、压缩。
线程池作用阶段(recv、decode、encode、send):

TOP3
TOP2
TOP
send
Encode
Compute
Decode
recv

redis使用线程池的条件:有多个客户端并发请求,并且有读写IO的问题(写日志业务、读大量数据等)。
redis线程池运行原理:
主线程收集所有的读事件,并放到一个队列中;线程池为每个线程都准备一个自己线程的队列;然后主线程将收集的事件分发到线程池IO线程的队列中,线程池的线程从自己的队列中取出任务、执行任务;主线程既是生产者也是消费者,主线程处理Compute阶段的业务逻辑。
在这里插入图片描述

TOP
收集读事件
写事件
pull
thread pool
thread#0
io_threads_list#0
thread#1
io_threads_list#1
thread#...
io_threads_list#...
pending队列
主线程
Compute

每个线程都有自己的队列原因:避免加锁。

skynet开源项目线程池

skynet是抽象进程(actor)并发模型。

总结

(1)线程池,就是固定线程数量,复用线程不销毁。
(2)线程池是一种生产者–消费者模型,某类任务特别耗时,会严重影响该线程处理其他任务,因此需要线程池。
(3)线程池是面向生产者的,生产者使用线程。线程池最好至少设计两个队列,任务队列和完成队列。
(4)通常,线程池的线程调度使用互斥锁(mutex)和条件变量(condition)。但也不一定非得使用condition(条件变量),可以只有互斥锁,比如redis,主线程获取mutex,其他线程会一直阻塞着,巧妙的利用互斥锁使其他线程休眠。
(5)线程池的线程数量选择,依据业务是IO密集型还是CPU密集型;假设CPU核心数为N,IO密集型一般设置为2*N+2,CPU密集型设为N。可以参考这样一个公式:(IO处理时间+CPU运行时间)*核心数/CPU运行时间。
(6)线程池的作用:复用线程资源,充分利用系统资源,异步执行耗时任务。

后言

本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接,详细查看详细的服务:C/C++服务器

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值