何为线程池,线程池有什么作用?
线程池是一种线程管理的机制,它允许在应用程序中预先创建一组线程,并使用这些线程来执行并发任务。线程池提供了一种优化线程使用和管理的方式,它通过减少线程创建和销毁的开销、限制线程数量以及通过任务队列来调度任务等方式来提高系统的性能和资源利用率。
提高性能:线程池允许并发地执行多个任务,因为预先创建的线程可以立即可用,避免了线程创建和销毁的开销。并且通过合理配置线程池的线程数量,避免了过多的线程竞争,从而提高系统的响应速度和吞吐量。
提高资源利用率:线程池可以限制系统中线程的数量,防止系统因过多的线程而耗尽CPU、内存等资源。通过复用线程,线程池可以更好地管理系统资源,提高资源的利用率。
任务调度与管理:线程池使用任务队列来接收和存储待执行的任务,对任务进行调度和分发给可用的线程。通过任务队列,线程池可以有效地管理任务的执行顺序、优先级以及异常情况的处理。
控制并发度:通过限制线程池中的线程数量,可以控制并发执行的任务数量,避免系统资源被过度占用。这样可以避免系统资源的饱和和过载,提高系统的稳定性。
一般在什么情况下值得使用线程池,什么时候反而不应使用呢?
- 需要处理大量并发任务:当应用程序需要同时处理大量的并发任务时,使用线程池可以更好地管理和调度这些任务,提高系统的并发处理能力。
- 任务执行时间较长:如果任务的执行时间较长,创建和销毁线程的开销相对较高,使用线程池可以避免频繁的创建和销毁线程,提高效率。
- 资源有限:在资源有限的环境下,如服务器端应用,有限的线程资源需要被合理利用。使用线程池可以限制并控制线程的数量,防止资源被过度占用。
- 异步任务:线程池可以用于执行异步任务,可以提交任务后立即返回,并通过回调或者Future对象来获取任务的执行结果。
- 任务之间相互依赖:如果任务之间存在较强的依赖关系,需要等待前一个任务完成后才能执行后续任务,使用线程池可能无法满足这种需求,可能需要其他的并发编程模型。
- 线程任务执行时间非常短暂:如果任务的执行时间非常短暂,线程池的线程创建和调度开销可能会超过执行任务的时间,反而会降低性能。
- 任务需要严格控制执行时机:如果需要严格控制任务的执行时机,如在特定时刻执行任务或者需要精确的任务调度时间,使用线程池可能无法满足这种需求。
让我们来通过代码pthread库制作一个简单的线程池吧
在这里我先放出头文件:
#ifndef MANAGEMENT_H
#define MANAGEMENT_H
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_POOL_SIZE 10
typedef struct {
void (*function)(void* arg);
void* arg;
} ThreadPoolTask;
typedef struct {
pthread_t threads[THREAD_POOL_SIZE]; // 存储线程ID的数组
int thread_count; // 线程数量
int task_count; // 任务数量
int shutdown; // 标志线程池是否关闭
ThreadPoolTask* tasks; // 存储任务的数组指针
pthread_mutex_t mutex; // 互斥锁,用于对线程池进行同步
pthread_cond_t condition; // 条件变量,用于线程间的等待和唤醒
} ThreadPool;
void threadPoolInit(ThreadPool* pool);
int threadPoolDestroy(ThreadPool* pool);
void threadPoolAddTask(ThreadPool* pool, void (*function)(void* arg), void* arg);
void* threadPoolThread(void* threadData);
void taskFunction(void* arg);
void threadPoolAdjustThread(ThreadPool* pool, int numThreads, int operation);
#endif
THREAD_POOL_SIZE是线程的总数量,就是创建线程池时,一共池子里会有多少线程。
ThreadPoolTask 结构体:
- void (function)(void arg):这是一个函数指针,指向一个以 void* 类型的参数为输入,没有返回值的函数。该函数指针用于执行具体的任务。
- void* arg:这是一个指针类型的参数,用于传递给任务函数的实际参数。任务函数在执行时可以通过这个参数访问和操作特定数据。 ThreadPoolTask 结构体用于表示线程池中的任务,并且包含了任务函数指针和参数,以便在执行任务时传递给任务函数进行执行。
ThreadPool 结构体:
- pthread_t threads[THREAD_POOL_SIZE]:这是一个数组,用于存储线程ID,大小为 THREAD_POOL_SIZE。在线程池初始化时,会创建一定数量的线程并将其ID存储在这个数组中。这些线程将负责执行任务。
- int thread_count:表示线程池中当前的线程数量。
- int task_count:表示线程池中当前的任务数量。
- int shutdown:表示线程池是否关闭的标志,当该标志位为非零值时,线程池将不再接受新的任务。
- ThreadPoolTask* tasks:这是一个指针类型的成员,指向存储任务的数组。实际上,这个指针指向了一块内存区域,其中存储了多个 ThreadPoolTask 结构体的实例。
- pthread_mutex_t mutex:互斥锁,用于对线程池进行同步。线程在对线程池中的数据进行修改时,需要先获得互斥锁,以保证线程安全性。
- pthread_cond_t condition:条件变量,用于线程间的等待和唤醒。当线程池中任务为空时,线程将等待条件变量的触发,直到有新任务被添加进来后被唤醒。 ThreadPool 结构体用于管理线程池的状态,并提供线程间的同步和协调机制。它包含了线程和任务的数量,以及用于同步的互斥锁和条件变量。 ThreadPoolTask 结构体则用于封装任务函数和参数,以便在线程池的线程中传递和执行任务。通过这两个结构体的组合,线程池能够有效地管理和调度任务的执行。
下面是C文件
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "management.h"
// 初始化线程池
void threadPoolInit(ThreadPool* pool) {
pool->thread_count = 0; // 初始化线程计数为0
pool->task_count = 0; // 初始化任务计数为0
pool->shutdown = 0; // 初始化关闭标志为0,表示线程池未关闭
pool->tasks = (ThreadPoolTask*)malloc(sizeof(ThreadPoolTask) * THREAD_POOL_SIZE); // 为任务数组分配内存空间,大小为线程池大小
// 初始化互斥锁和条件变量
pthread_mutex_init(&(pool->mutex), NULL);
pthread_cond_init(&(pool->condition), NULL);
int i;
for (i = 0; i < THREAD_POOL_SIZE; i++) {
// 创建线程并增加线程计数
pthread_create(&(pool->threads[i]), NULL, threadPoolThread, (void*)pool);
pool->thread_count++;
}
}
int threadPoolDestroy(ThreadPool* pool) {
if (pool == NULL) {
return 1; // 确保线程池指针不为空
}
if (pool->shutdown) {
return 1; // 如果关闭标志为1,直接返回,避免重复销毁线程池
}
// 设置关闭标志为1,表示线程池将要关闭
pool->shutdown = 1;
// 唤醒所有等待在条件变量上的线程
pthread_cond_broadcast(&(pool->condition));
int i;
for (i = 0; i < pool->thread_count; i++) {
// 等待所有线程结束
pthread_join(pool->threads[i], NULL);
}
// 释放任务数组内存
if (pool->tasks != NULL) {
free(pool->tasks);
}
// 销毁互斥锁
pthread_mutex_destroy(&(pool->mutex));
// 销毁条件变量
pthread_cond_destroy(&(pool->condition));
return 0;
}
// 添加任务到线程池
void threadPoolAddTask(ThreadPool* pool, void (*function)(void* arg), void* arg) {
pthread_mutex_lock(&(pool->mutex)); // 加锁互斥锁
// 获取任务位置
ThreadPoolTask* task = &(pool->tasks[pool->task_count]);
task->function = function;
task->arg = arg;
pool->task_count++; // 增加任务计数
pthread_cond_signal(&(pool->condition)); // 唤醒等待的线程
pthread_mutex_unlock(&(pool->mutex)); // 解锁互斥锁
}
// 线程函数,用于执行任务
void* threadPoolThread(void* threadData) {
ThreadPool* pool = (ThreadPool*)threadData;
while (1) {
pthread_mutex_lock(&(pool->mutex)); // 加锁互斥锁
// 等待任务或关闭标志
while (pool->task_count == 0 && !pool->shutdown) {
pthread_cond_wait(&(pool->condition), &(pool->mutex)); // 等待条件变量唤醒
}
// 如果关闭标志为1,退出线程
if (pool->shutdown) {
pthread_mutex_unlock(&(pool->mutex)); // 解锁互斥锁
pthread_exit(NULL); // 退出线程
}
// 获取任务并减少任务计数,解锁互斥锁
ThreadPoolTask task = pool->tasks[pool->task_count - 1];
pool->task_count--;
pthread_mutex_unlock(&(pool->mutex)); // 解锁互斥锁
// 执行任务函数
(*(task.function))(task.arg);
}
pthread_exit(NULL);
}
void threadPoolAdjustThread(ThreadPool* pool, int numThreads, int operation) {
if (pool == NULL || numThreads == 0) {
return;
}
pthread_mutex_lock(&(pool->mutex)); // 加锁互斥锁
if (operation == 1) { // 增加线程数量
// 计算要增加的线程数量
int newThreadCount = pool->thread_count + numThreads;
if (newThreadCount > THREAD_POOL_SIZE) {
newThreadCount = THREAD_POOL_SIZE;
}
int i;
for (i = pool->thread_count; i < newThreadCount; i++) {
// 创建线程并增加线程计数
pthread_create(&(pool->threads[i]), NULL, threadPoolThread, (void*)pool);
pool->thread_count++;
}
} else if (operation == 0) { // 减少线程数量
// 计算要减少的线程数量
int reducedThreadCount = pool->thread_count - numThreads;
if (reducedThreadCount <= 0) {
reducedThreadCount = 0;
}
int i;
for (i = pool->thread_count - 1; i >= reducedThreadCount; i--) {
// 取消线程并减少线程计数
pthread_cancel(pool->threads[i]);
pthread_join(pool->threads[i], NULL);
pool->thread_count--;
}
}
pthread_mutex_unlock(&(pool->mutex)); // 解锁互斥锁
}
threadPoolInit(ThreadPool* pool)
- 功能:初始化线程池。
- 实现方法:
- 设置线程计数、任务计数和关闭标志为零。
- 分配内存空间以存储任务数组,大小为线程池大小。
- 初始化互斥锁和条件变量。
- 创建指定数量的线程,并增加线程计数。
int threadPoolDestroy(ThreadPool* pool)
- 功能:销毁线程池。
- 实现方法:
- 检查线程池是否为NULL,如果是则返回1。
- 检查关闭标志,如果为1则表示线程池已经关闭,直接返回1以避免重复销毁线程池。
- 设置关闭标志为1以表示线程池将要关闭。
- 唤醒所有等待在条件变量上的线程。
- 循环等待所有线程结束。
- 释放任务数组的内存。
- 销毁互斥锁和条件变量。
- 返回0表示销毁成功。
void threadPoolAddTask(ThreadPool* pool, void (*function)(void* arg), void* arg)
- 功能:将任务添加到线程池。
- 实现方法:
- 加锁互斥锁以保护共享资源。
- 获取可用的任务位置。
- 将任务函数和参数存储在任务数组中。
- 增加任务计数。
- 唤醒等待的线程。
- 解锁互斥锁。
void* threadPoolThread(void* threadData)
- 功能:线程函数,用于执行任务。
- 实现方法:
- 将线程数据转换为线程池类型。
- 循环执行以下逻辑:
- 加锁互斥锁。
- 在任务计数为零且关闭标志为否时等待条件变量唤醒。
- 如果关闭标志为1,表示线程池将要关闭,解锁互斥锁并退出线程。
- 获取任务,并减少任务计数,解锁互斥锁。
- 执行任务函数。
- 退出线程。
void threadPoolAdjustThread(ThreadPool* pool, int numThreads, int operation)
- 功能:调整线程数量。
- 实现方法:
- 检查线程池和要调整的线程数量是否为NULL或零,如果是则返回。
- 加锁互斥锁。
- 如果操作为1(增加线程数量):
- 计算要增加的线程数量,并确保不超过线程池大小。
- 循环从线程计数开始增加线程直到新线程计数达到要增加的线程数量。
- 如果操作为0(减少线程数量):
- 计算要减少的线程数量,并确保不小于零。
- 循环从线程计数减一开始取消线程直到线程计数达到要减少的线程数量。
- 解锁互斥锁。 每个函数都有特定的功能,利用互斥锁和条件变量来保证线程安全性。线程池提供了任务的添加、执行和调整线程数量的功能。
范例
下面我写了一个范例,例如用线程池完成一份文件的读取的任务指针函数,到时候我们再主函数里面使用以上函数调用的时候以这个指针函数作为参数来实现对文件的读取。
#ifndef EXAMPLE_H
#define EXAMPLE_H
// 包含必要的头文件
#include <stdio.h>
#include <sys/stat.h>
#include "management.h"
// 定义常量、宏和结构体等
#define BUFFER_SIZE 1024
typedef struct {
const char *file_name;
unsigned char *array;
int start_index;
int size;
} ThreadArgs;
void *read_file(void *arg);
int getFileSize(const char *filename);
#endif /* EXAMPLE_H */
#include "example.h"
void *read_file(void *arg) {
ThreadArgs *args = (ThreadArgs *)arg;
const char *file_name = args->file_name;
unsigned char *array = args->array;
int start_index = args->start_index;
int size = args->size;
// 打开文件
FILE *file = fopen(file_name, "r");
if (file == NULL) {
printf("文件不存在\n");
pthread_exit(NULL);
}
// 定位文件指针到指定位置
fseek(file, start_index, SEEK_SET);
// 读取文件内容
int bytes_read = fread(array + start_index, 1, size, file);
if (bytes_read == 0) {
printf("文件读取出错\n");
}
// 关闭文件
fclose(file);
pthread_exit(NULL);
}
int getFileSize(const char *filename) {
struct stat st;
if (stat(filename, &st) == 0) {
return st.st_size;
}
return -1; // 文件不存在或获取失败
}
主函数
#include "management.h"
#include "example.h"
#include "string.h"
int main()
{
//初始化线程池
ThreadPool pool;
threadPoolInit(&pool);
//文件名称
const char *file_name = "/home/gec/HDD_STORAGE/Multiprocessing_RW/ztext.txt";
//给读取文件大小
int file_size = getFileSize(file_name);
if(file_size != -1)
{
printf("文件大小:%d 字节\n", file_size);
} else
{
printf("获取文件大小失败\n");
}
unsigned char *array = (unsigned char *)malloc(file_size);
if(array == NULL)
{
perror("数组空间创建失败");
exit(1);
}
memset(array,0,sizeof(array));
//给每个线程分配任务量
int thread_size = file_size / THREAD_POOL_SIZE;
int remainder = file_size % THREAD_POOL_SIZE;
//给每个线程的arg参数配置
ThreadArgs thread_args[THREAD_POOL_SIZE];
for (int i = 0; i < THREAD_POOL_SIZE; i++) {
thread_args[i].file_name = file_name;
thread_args[i].array = array;
thread_args[i].start_index = i * thread_size;
thread_args[i].size = thread_size + (i == THREAD_POOL_SIZE - 1 ? remainder : 0);
//pthread_create(&pool.threads[i], NULL, read_file, (void *)&thread_args[i]);
threadPoolAddTask(&pool, (void *)read_file, &thread_args[i]);
}
// 等待所有线程完成
// for (int i = 0; i < THREAD_POOL_SIZE; i++) {
// pthread_join(pool.threads[i], NULL);
// }
// 打印结果
sleep(1);
for (int i = 0; i < file_size; i++) {
printf("%c", array[i]);
}
printf("\n");
// 释放内存
free(array);
threadPoolDestroy(&pool); // 销毁线程池
return 0;
}
// // 示例任务函数
// void taskFunction(void* arg) {
// int taskNumber = *(int*)arg;
// printf("正在执行任务编号:%d\n", taskNumber);
// }
// int main() {
// ThreadPool pool;
// threadPoolInit(&pool); // 初始化线程池
// int i;
// for (i = 0; i < 10; i++)
// {
// int* arg = malloc(sizeof(int));
// *arg = i;
// threadPoolAddTask(&pool, taskFunction, arg); // 添加任务到线程池
// }
// sleep(2); // 暂停2秒
// threadPoolDestroy(&pool); // 销毁线程池
// return 0;
// }
只要做好每个线程都要读取文件大小的分配就好啦,然后再把参数传递到thread_args结构体内,就可以继续传递到任务函数内,用来分配和规划每个任务要干什么。
结果
这是要读取的范本。
文件读取成功。
编译脚本
最后在这里再顺便给一个shell脚本方便大家编译,里面的文件名字自己改动下就好了。
echo "删除原有demo..."
rm test
echo "编译中..."
gcc management.c main.c example.c -o test -pthread
# 检查编译是否成功
if [ $? -eq 0 ]; then
echo "编译成功"
# 执行命令
echo "运行demo..."
start=$(date +%s) # 记录开始时间
./test
# 检查命令是否执行成功
if [ $? -eq 0 ]; then
end=$(date +%s) # 记录结束时间
diff=$(( $end - $start )) # 计算时间差
echo "运行成功,累计花费时间: $diff 秒"
else
echo "命令执行失败"
exit 1
fi
else
echo "编译失败"
exit 1
fi
# 完成
echo "完成"
编译。
./test
本期分享就到这里,我们共同成长、相互学习,无论是分享自己的经验还是倾听他人的建议,都能让我们更加成熟和专业。让我们一起在这个充满活力的社区中共同进步!