C 语言版线程池
一、初始线程池
1.1 何为线程池?
我们先来打个比方,线程池就好像一个工具箱,我们每次需要拧螺丝的时候都要从工具箱里面取出一个螺丝刀来。有时候需要取出一个来拧,有时候螺丝多的时候需要多个人取出多个来拧,拧完自己的螺丝那么就会把螺丝刀再放回去,然后别人下次用的时候再取出来用。
说白了线程池就是相当于「提前申请了一些资源,也就是线程」,需要的时候就从线程池中取出线程来处理一些事情,处理完毕之后再把线程放回去。
1.2 为什么要使用线程池?
我们来思考一个问题,为什么需要线程池呢?假如没有线程池的话我们每次调用线程是什么样子的?
显然首先是先创建一个线程,然后把任务交给这个线程,最后把这个线程销毁掉。这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程是需要消耗时间的。
那么如果我们改用线程池的话,在程序运行的时候就会首先创建一批线程,然后交给线程池来管理。有需要的时候我们从线程池中取出线程用于处理任务,用完后我们再将其回收到线程池中,这样是不是就避免了每次都需要创建和销毁线程这种耗时的操作。
有人会说你使用线程池一开始就消耗了一些内存,之后一直不释放这些内存,这样岂不是有点浪费。其实这是类似于空间换时间的概念,我们确实多占用了一点内存但是这些内存和我们珍惜出来的这些时间相比,是非常划算的。
池的概念是一种非常常见的空间换时间的概念,除了有线程池之外还有进程池、内存池等等。其实他们的思想都是一样的就是我先申请一批资源出来,然后就随用随拿,不用再放回来。
1.3 如何设计线程池
线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:
- 任务队列:存储需要处理的任务,由工作的线程来处理这些任务。
- 通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
- 已处理的任务会被从任务队列中删除
- 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
- 工作的线程(任务队列任务的消费者):若干个,一般情况下根据 CPU 的核数来确定。
- 线程池中维护了一定数量的工作线程,他们的作用是不停的读任务队列,从里边取出任务并处理
- 工作的线程相当于是任务队列的消费者角色
- 如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞)
- 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
- 管理者线程(不处理任务队列中的任务):1 个
- 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
- 当任务过多的时候,可以适当的创建一些新的工作线程
- 当任务过少的时候,可以适当的销毁一些工作的线程
二、C 语言版线程池
由于本篇是对线程池的简单介绍,所以简化了一下线程池的模型,将 1.3 中的「3. 管理者线程」的角色给去除了。
2.1 结构体定义
2.1.1 任务结构体
/* 任务结构体 */
typedef struct
{
void (*function)(void *);
void *arg;
} threadpool_task_t;
2.1.2 线程池结构体
/* 线程池结构体 */
typedef struct
{
pthread_mutex_t lock; // 线程池锁,锁整个的线程池
pthread_cond_t notify; // 条件变量,用于告知线程池中的线程来任务了
int thread_count; // 线程池中的工作线程总数
pthread_t *threads; // 线程池中的工作线程
int started; // 线程池中正在工作的线程个数
threadpool_task_t *queue; // 任务队列
int queue_size; // 任务队列能容纳的最大任务数
int head; // 队头 -> 取任务
int tail; // 队尾 -> 放任务
int count; // 任务队列中剩余的任务个数
int shutdown; // 线程池状态, 0 表示线程池可用,其余值表示关闭
} threadpool_t;
-
thread_count 和 started 的区别:
- 初始化线程池的时候会创建一批线程(假设创建 n 个),此时 thread_count = started = n
- 当线程池运行过程中可能需要关闭一些线程(假设关闭 m 个),则会销毁这些线程,并 started -= n,但 thread_count 保持不变
- 即 thread_count 表示线程池中的申请的线程个数,而 started 表示当前能用的线程个数
-
shutdown 的作用:如果需要销毁线程池,那么必须要现将所有的线程退出才可销毁,而 shutdown 就是用于告知正在工作中的线程,线程池是否关闭用的。关闭方式又分为两种:一种是立即关闭,即不管任务队列中是否还有任务;另一种是优雅的关闭,即先处理完任务队列中的任务后再关闭。这两种方式可通过设置 shutdown 的不同取值即可实现:
typedef enum { immediate_shutdown = 1, // 立即关闭线程池 graceful_shutdown = 2 // 等线程池中的任务全部处理完成后,再关闭线程池 } threadpool_shutdown_t;
2.2 函数定义
2.2.1 ThreadPool_Init
函数原型:int ThreadPool_Init(int thread_count, int queue_size, threadpool_t **ppstThreadPool);
头 文 件:#include "ThrdPool.h"
函数功能:初始化线程池
参数描述:
- thread_count:入参,代表此次创建的线程池中的线程个数
- queue_size:入参,代表任务队列大小
- ppstThreadPool:出参,如果创建成功,则代表创建好的线程池,否则为 NULL
返 回 值:成功返回 E_SUCCEED,失败返回 E_ERROR
2.2.2 ThreadPool_Dispatch
函数原型:int ThreadPool_Dispatch(threadpool_t *pstThreadPool, void (*function)(void *), void *arg);
头 文 件:#include "ThrdPool.h"
函数功能:向线程池的任务队列中分发任务
参数描述:
- pstThreadPool:入参,代表创建好的线程池
- function:入参,表示任务
- arg:入参,代表 function 的参数
返 回 值:成功返回 E_SUCCEED,失败返回 E_ERROR
2.2.2 Threadpool_Destroy
函数原型:void Threadpool_Destroy(threadpool_t *pool, threadpool_shutdown_t shutdown_mode);
头 文 件:#include "ThrdPool.h"
函数功能:销毁线程池
参数描述:
- pool:入参,表示需要销毁的线程池
- shutdown_mode:入参,表示关闭模式,有两种取值
2.3 源码
2.3.1 ThrdPool.h
#ifndef __THRDPOOL_H__
#define __THRDPOOL_H__
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define DEBUG(format, args...) \
printf("[%s:%d] "format"\n", \
__FILE__, \
__LINE__, \
##args)
#define MAX_THREADS 16 // 线程池最大工作线程个数
#define MAX_QUEUE 256 // 线程池工作队列上限
#define E_SUCCEED 0
#define E_ERROR 112
#define SAFE_FREE(ptr) \
if (ptr) \
{ \
free(ptr); \
ptr = NULL; \
}
/* 任务结构体 */
typedef struct
{
void (*function)(void *);
void *arg;
} threadpool_task_t;
/* 线程池结构体 */
typedef struct
{
pthread_mutex_t lock; // 线程池锁,锁整个的线程池
pthread_cond_t notify; // 条件变量,用于告知线程池中的线程来任务了
int thread_count; // 线程池中的工作线程总数
pthread_t *threads; // 线程池中的工作线程
int started; // 线程池中正在工作的线程个数
threadpool_task_t *queue; // 任务队列
int queue_size; // 任务队列能容纳的最大任务数
int head; // 队头 -> 取任务
int tail; // 队尾 -> 放任务
int count; // 任务队列中剩余的任务个数
int shutdown; // 线程池状态, 0 表示线程池可用,其余值表示关闭
} threadpool_t;
typedef enum
{
immediate_shutdown = 1, // 立即关闭线程池
graceful_shutdown = 2 // 等线程池中的任务全部处理完成后,再关闭线程池
} threadpool_shutdown_t;
int ThreadPool_Init(int thread_count, int queue_size, threadpool_t **ppstThreadPool);
int ThreadPool_Dispatch(threadpool_t *pstThreadPool, void (*function)(void *), void *arg);
void Threadpool_Destroy(threadpool_t *pool, threadpool_shutdown_t shutdown_mode);
#endif
2.3.2 ThrdPool.c
#include <stdio.h>
#include "ThrdPool.h"
#define THREAD_COUNT 4
#define QUEUE_SIZE 128
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化锁,用于保证第16行的完整输出
void func(void *arg)
{
static int num = 0;
pthread_mutex_lock(&mutex);
// 为方便观察,故特意输出该语句,并使用num来区分不同的任务
DEBUG("这是执行的第 %d 个任务", ++num);
usleep(100000); // 模拟任务耗时,100ms
pthread_mutex_unlock(&mutex);
return;
}
int main()
{
int iRet;
threadpool_t *pool;
iRet = ThreadPool_Init(THREAD_COUNT, QUEUE_SIZE, &pool);
if (iRet != E_SUCCEED)
{
return 0;
}
int i;
for (i = 0; i < 20; i++) // 生产者,向任务队列中塞入 20 个任务
{
ThreadPool_Dispatch(pool, func, NULL);
}
usleep(500000);
// Threadpool_Destroy(pool, immediate_shutdown); // 立刻关闭线程池
Threadpool_Destroy(pool, graceful_shutdown); // 等任务队列中的任务全部执行完毕再关闭
return 0;
}
2.3.3 main.c
#include <stdio.h>
#include "ThrdPool.h"
#define THREAD_COUNT 4
#define QUEUE_SIZE 128
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化锁,用于保证第 15 行的完整输出
void func(void *arg)
{
static int num = 0;
pthread_mutex_lock(&mutex);
DEBUG("这是执行的第 %d 个任务", ++num); // 为方便观察,故特意输出该语句,并使用num来区分不同的任务
usleep(100000); // 模拟任务耗时,100ms
pthread_mutex_unlock(&mutex);
return;
}
int main()
{
int iRet;
threadpool_t *pool;
iRet = ThreadPool_Init(THREAD_COUNT, QUEUE_SIZE, &pool);
if (iRet != E_SUCCEED)
{
return 0;
}
int i;
for (i = 0; i < 20; i++) // 生产者,向任务队列中塞入 20 个任务
{
ThreadPool_Dispatch(pool, func, NULL);
}
usleep(500000);
// Threadpool_Destroy(pool, immediate_shutdown); // 立刻关闭线程池
Threadpool_Destroy(pool, graceful_shutdown); // 等任务执行完毕后方可关闭
return 0;
}