C语言实现线程池
任务的存储
-
任务结构体
任务是添加到任务队列的函数,因此需要设计一个结构体包含函数指针与函数参数//任务结构体 typedef struct Task { void (*function)(void* arg);//函数指针,返回值为void void* arg;//函数的参数 }Task;
-
任务队列怎么实现
直接使用2个变量记录队列头尾,手动实现队列。
线程池的实现
-
为了管理者能够合理对线程数进行调整,需要记录当前当前存活的线程数与忙线程数
-
为了各线程安全访问共享资源,如忙线程数量、当前存活线程数量,需要使用互斥锁,并使用条件变量实现生产者消费者模型。
//线程池结构体 struct ThreadPool { Task* taskQ;//任务队列,由队头队尾下标维护成一个环形队列 int queueCapacity;//任务队列的容量 int queueSize;//当前任务个数 int queueFront;//队头下标 int queueRear;//队尾下标 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;//单独用于锁busyNum,因为busyNum变化很频繁,如果每次都直接锁整个线程池的不太好 pthread_cond_t notFull;//条件变量,任务队列不满时唤醒生产者线程 pthread_cond_t notEmpty;//任务队列不为空时唤醒工作线程(消费者) int shutdown;//要不要销毁线程池,销毁为1,不销毁为0 };
-
线程池初始化
pthread_create创建出一个管理者线程、多个工作线程,并将线程ID存放在数组中。//创建线程 pthread_create(&pool->managerID, NULL, manager, pool);//管理者线程 //工作线程 for (int i = 0; i < min; i++) {//工作线程的函数应该传入pool,到时候可以直接取到pool里面的任务队列taskQ和一些锁 pthread_create(&pool->threadIDs[i], NULL, worker, pool); }
工作线程与管理者线程
-
工作线程(消费者)的函数
循环读取任务队列,如果没任务了且线程池没关闭,那么就阻塞。否则,从头部取出一个任务来执行。
为了管理者能让工作线程自杀,在工作线程阻塞被唤醒后,会对exitNum值进行判断,来决定是否线程自杀。//工作线程的函数 void* worker(void* arg) { ...... //2.循环读取任务队列,而任务队列是共享资源,需要互斥访问 while (1) { pthread_mutex_lock(&pool->mutexPool);//mutexPool拿来专门锁整个线程池 //当前任务个数为0且线程池未被关闭 while (pool->queueSize == 0 && !pool->shutdown) { //那么应该阻塞工作线程 pthread_cond_wait(&pool->notEmpty, &pool->mutexPool); //判当前需不需要销毁线程 if (pool->exitNum > 0)//在管理者发现闲着的线程过多时,会设置pool->exitNum值 { //pthread_cond_wait在wait时自动释放锁,在被唤醒后又自动加锁了 //为了避免死锁,这里需要在线程自杀前释放锁 pool->exitNum--;//不管工作线程是否真的自杀,这里都应该--。否则,exitNum始终非0,所有被唤醒的线程都来自杀了 if (pool->liveNum > pool->minNum) { pool->liveNum--;//马上线程自杀了,活着的线程数应该-1 pthread_mutex_unlock(&pool->mutexPool); threadExit(pool);//线程自杀 } } } 当前是正常的情况,工作线程进行消费 ...... task.function(task.arg); ...... } return NULL; }
-
管理者线程的函数
只要线程池没关闭,就每隔3秒检测一次,添加或减少工作线程数-
添加线程:任务个数 > 存活线程个数
//添加线程:规定,当前任务个数>存活线程数 && 存活线程数<最大线程数,则添加线程 if (queueSize > liveNum && liveNum < pool->maxNum)//maxNum是永远不会被改变的,因此无需互斥访问 { pthread_mutex_lock(&pool->mutexPool);//因为要操作pool->liveNum int counter = 0; //从存储线程id的数组中找到空闲位置 来存储新创建的线程id for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum; i++) { if (pool->threadIDs[i] == 0)//该位置的内存还没有存储线程id { pthread_create(&pool->threadIDs[i], NULL, worker, pool);//创建线程 counter++; pool->liveNum++; } } pthread_mutex_unlock(&pool->mutexPool); }
-
销毁线程:设置exitNum值,再唤醒正在阻塞的线程,它们看到exitNum非0就自杀
//销毁线程:规定,忙线程数*2<存活线程数 && 存活线程数>最小线程数 if (busyNum*2<liveNum && liveNum>pool->minNum) { pthread_mutex_lock(&pool->mutexPool); pool->exitNum = NUMBER; pthread_mutex_unlock(&pool->mutexPool); //让工作线程自杀:唤醒wait的工作线程,工作线程会去检查pool->exitNum,非0则工作线程自杀 for (int i = 0; i < NUMBER; i++) { pthread_cond_signal(&pool->notEmpty); } }
-
主线程
-
主线程中添加任务
int main() { //创建线程池 ThreadPool* pool = threadPoolCreate(3, 10, 100); //添加任务 for (int i = 0; i < 100; i++) { int* num = (int*)malloc(sizeof(int)); *num = i + 100; threadPoolAdd(pool, taskFunc, num); } sleep(30); //销毁线程池 thradPoolDestory(pool); return 0; }
改写成C++版线程池
- 在C语言版中,线程池与任务队列的实现混杂在一起。现在,将它们分开写成两个类ThreadPool和TaskQueue
- 此前手动实现并管理队列,现在使用容器queue来自动管理。
- 此前对线程池进行操作时需要将线程池对象传入,现在直接使用默认传过来的this指针即可获得线程池对象。
- 使用模板来表明任务函数的类型
std::counting_semaphore<3> sema(0)
表示信号量最大值为3,初始值为0
acquire()
: 如果信号量的内部计数大于0,那么这个方法会减少计数并立即返回。否则,这个方法会阻塞,直到其他线程调用release()方法增加了计数。
try_acquire()
: 这个方法尝试减少信号量的计数。如果计数大于0,那么这个方法会减少计数并返回true。否则,这个方法不会阻塞,而是立即返回false。
release(n = 1)
: 这个方法增加信号量的计数。参数n指定了要增加的数量,默认为1。如果有其他线程在等待信号量(即它们调用了acquire()方法并被阻塞),那么这个方法会唤醒那些线程。