目的:
- 学习消费者和生产者模型
- 体验锁、条件变量在线程同步中的作用
概念:
线程池是什么?
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
管理者线程? 实时监控待处理任务与消费者线程的状况,依据规则添加or销毁消费者线程
消费者线程? 从任务队列中取出任务,并执行
生产者线程? 向任务队列中添加任务
任务队列? 包含容量、当前任务数、任务顶、任务底等属性,本代码采用循环队列
功能函数
// 创建线程池
Threadpool* threadPoolCreate(int min, int max, int queuesize);
// 销毁线程池
int threadPoolDestroy(Threadpool* pool);
// 向线程池添加任务
void threadPoolAddTask(Threadpool* pool, void(*func)(void*), void* arg);
// 获取线程池中工作的线程的个数
int threadPoolBusyNum(Threadpool* pool);
// 获取线程池中活着的线程的个数
int threadPoolAliveNum(Threadpool* pool);
///
// 管理者线程
void* manager(void* arg);
// 消费者线程
void* worker(void* arg);
// 线程退出函数
void threadExit(Threadpool* pool);
程序
1. main.c
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include "threadpool.h"
void taskFunc(void* arg)
{
int num = *(int*)arg; // 取堆内存的数据
printf("thread %ld is working, number = %d\n",
pthread_self(), num, pthread_self());
sleep(1); // 老师改为1s,可以让工作的线程多一点
}
int main()
{
// 创建线程池
Threadpool* pool = threadPoolCreate(3, 10, 100);
// 添加任务
for (int i = 0; i < 100; ++i)
{
int* num = (int*)malloc(sizeof(int)); // 最好是初始化为堆内存
*num = i+100;
threadPoolAddTask(pool, taskFunc, num);
printf("num = %d\n", *num);
}
// 让主线程睡眠一段时间,保证工作线程的任务已经处理完毕,实际场景中主线程肯定有别的事情干
sleep(30);
// 销毁线程池
threadPoolDestroy(pool);
return 0;
}
2. 线程池结构 Threadpool
typedef struct Task {
void (*function)(void* arg);
void* arg;
}Task;
typedef struct Threadpool {
// 任务队列
Task* taskQ; // 任务队列
int queueCapacity; // 任务容量
int queueSize; // 任务数
int queueFront; // 任务队首
int queueBack; // 任务队尾
// 线程
pthread_t managerID; // 管理者线程ID
pthread_t* threadIDs; // 消费者线程ID组
int minNum; // 最大线程数
int maxNum; // 最小线程数
int busyNum; // 工作线程数
int liveNum; // 存活线程数
int exitNum; // 待销毁线程数
// 锁
pthread_mutex_t mutexpool; // 整个线程池的锁
pthread_mutex_t mutexbusy; // 忙线程数的锁(忙线程常被修改)
// 条件变量
pthread_cond_t notEmpty;
pthread_cond_t notFull;
int shutdown; // 是不是要销毁线程池, 销毁为1, 不销毁为0
}Threadpool;
问题与解决
创建线程池时,遇到错误时,采用dowhile,break解决已分配内存的释放问题
销毁线程时,首先唤醒他们,通过标志位让他们自行销毁
一些编程经验
- 线程同步时的临界资源,读取或者修改的时候是一定要加锁的,但是为了让加锁解锁对程序的影响达到最小,应当用一个临时变量把值取出来(取数据加锁,取完解锁)在栈内计算时就用这个临时变量;同理写入的时候也一样
- 指针真的很好用,能用指针尽量都要用。但要注意,在退出当前栈的时候,指针变量会被销毁,但指针所指的那部分堆空间不会,所以如果堆空间不需要了的话,要及时销毁;如果堆空间还要,记得把指针变量传递出来,供后续其他函数或者对象去使用。