线程池与任务的理解:
线程池中存储的是线程。线程是操作系统能够进行运算调度的最小单位,它是 CPU 调度和分派的基本单位。在一个多线程的程序中,每个线程都是独立运行的,它们可以并发地执行不同的任务。
在一个线程池中,通常会创建一组预先初始化的线程,这些线程会等待从任务队列中取出任务并执行。这样可以避免频繁地创建和销毁线程,提高了程序的效率和性能。
而任务队列则用于存储待执行的任务。当程序需要执行一个任务时,它会将任务添加到任务队列中,然后线程池中的某个空闲线程会从队列中取出任务并执行。这样,任务的执行和线程的管理被有效地分离开来,提高了程序的可维护性和扩展性。
概念:
通俗的讲就是一个线程的池子,可以循环的完成任务的一组线程集合
必要性:
我们平时创建一个线程,完成某一个任务,等待线程的退出。但当需要创建大量的线程时,假设T1为创建线程时间,T2为在线程任务执行时间,T3为线程销毁时间,当 T1+T3 > T2,这时候就不划算了,使用线程池可以降低频繁创建和销毁线程所带来的开销,任务处理时间比较短的时候这个好处非常显著。
线程池的基本结构:
1 任务队列,存储需要处理的任务,由工作线程来处理这些任务
2 线程池工作线程,它是任务队列,任务的消费者,等待新任务的信号
线程池的实现:
1. 创建线程池的基本结构:
任务队列链表
typedef struct Task{
void *(*func)(void *arg);
void *arg;
struct Task *next;
}Task;
void *(*func)(void *arg);:这是一个函数指针成员 func,它指向一个函数,该函数的参数和返回值都是 void* 类型。
这种声明方式表示函数可以接受一个 void* 类型的参数,并返回一个 void* 类型的值。
这样设计的目的是为了能够传递不同类型的参数给任务函数,并且可以通过 void* 类型的返回值传递任意类型的结果。
void *arg;:这是一个 void* 类型的指针成员 arg,用于存储传递给任务函数的参数。
由于任务函数的参数类型是任意的,因此使用 void* 类型的指针可以灵活地传递各种类型的参数。
struct Task *next;:这是一个指向下一个任务的指针成员 next,用于构建任务队列。
通过这个指针,可以将任务按顺序链接起来,形成一个链表结构,以便线程池中的工作线程可以按顺序从任务队列中取出任务执行。
线程池结构体
注意:线程池是临界资源,所以需要加上互斥锁
如果线程池中的多个线程需要从任务队列中获取任务,那么在获取任务的操作中就需要加锁,确保每次只有一个线程能够成功获取任务,其他线程需要等待。
对于临界资源,确实需要使用互斥锁进行保护,以确保在任何时刻只有一个线程可以访问该资源,从而避免竞态条件和数据不一致的问题。
互斥锁(Mutex)是一种同步机制,它可以确保在任意时刻只有一个线程能够进入临界区(对共享资源的访问部分),而其他线程则必须等待。
在多线程编程中,临界资源可能是共享的数据结构、文件、网络连接等。
如果多个线程同时访问或修改这些共享资源,可能会导致数据的不一致性或者程序的错误行为。
通过在访问临界资源的代码块周围加上互斥锁,可以确保每次只有一个线程能够访问共享资源,从而避免竞争条件的发生。
需要注意的是,使用互斥锁需要遵循一定的规则,例如在访问临界资源之前先获取互斥锁,在访问完成后释放互斥锁等。
同时,互斥锁的使用也可能会带来一些开销,因此需要在设计和实现中考虑到性能和并发性等方面的权衡。
线程池结构体:
typedef struct ThreadPool{
pthread_mutex_t taskLock;
pthread_cond_t newTask;
pthread_t tid[POOL_NUM];
Task *queue_head;
int busywork;
}ThreadPool;
pthread_mutex_t taskLock;:这是一个互斥锁成员 taskLock,用于保护线程池中共享的数据结构,如任务队列和忙碌线程计数器。
在对这些共享资源进行操作时,需要先获取该互斥锁,以确保线程间的同步和数据的一致性。
pthread_cond_t newTask;:这是一个条件变量成员 newTask,用于在任务队列中添加新任务时通知等待的线程有任务可执行。
当有新任务添加到任务队列时,会通过这个条件变量通知线程池中等待的工作线程。
pthread_t tid[POOL_NUM];:这是一个线程 ID 数组成员 tid,用于存储线程池中每个工作线程的线程 ID。这个数组的大小由宏 POOL_NUM 定义,表示线程池中最多可以同时运行的工作线程数量。
Task *queue_head;:这是一个指向 Task 结构体的指针成员 queue_head,用于指向任务队列的头部。
通过这个指针,可以访问任务队列中的第一个任务,并且通过任务的 next 成员链接到下一个任务。
int busywork;:这是一个整型成员 busywork,用于记录当前正在执行任务的线程数量。
当有新任务添加到任务队列时,会增加 busywork 的值,当工作线程完成任务时,会减少 busywork 的值。
这个成员用于限制线程池中同时执行任务的数量,以防止超出预期的并发度。
下例子是简单的创建互斥锁的代码:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex; // 声明一个互斥锁变量
int main() {
// 初始化互斥锁
if (pthread_mutex_init(&mutex, NULL) != 0) {
printf("互斥锁初始化失败\n");
return 1;
}
// 在这里进行其他操作...
// 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
在这个示例中,pthread_mutex_init()
函数用于初始化一个互斥锁。它的第一个参数是指向要初始化的互斥锁变量的指针,第二个参数是一个指向 pthread_mutexattr_t
类型的指针,通常我们将其设置为 NULL
,表示使用默认的互斥锁属性。如果初始化失败,pthread_mutex_init()
将返回非零值。
在使用完互斥锁后,你还需要使用 pthread_mutex_destroy()
函数来销毁它,以释放相关资源。
2. 线程池的初始化:
pool_init()
{
创建一个线程池结构
实现任务队列互斥锁和条件变量的初始化
创建n个工作线程
}
ThreadPool *pool;
void pool_init()
{
pool = malloc(sizeof(ThreadPool));
pthread_mutex_init(&pool->taskLock,NULL);
pthread_cond_init(&pool->newTask,NULL);
pool->queue_head = NULL;
pool->busywork=0;
for(int i=0;i<POOL_NUM;i++)
{
pthread_create(&pool->tid[i],NULL,workThread,NULL);
}
}
pool = malloc(sizeof(ThreadPool));:分配了内存以存储 ThreadPool 结构体的实例,并将其地址赋给全局变量 pool。
这里使用了 malloc() 动态分配内存,以便在堆上创建一个新的线程池实例。
pthread_mutex_init(&pool->taskLock, NULL);:初始化了线程池的互斥锁 taskLock。pthread_mutex_init() 函数用于初始化互斥锁,并且这里将其锁定状态初始化为可用(即没有被锁定)。
第二个参数传入 NULL 表示使用默认的互斥锁属性。
pthread_cond_init(&pool->newTask, NULL);:初始化了线程池的条件变量 newTask。
条件变量用于在线程等待某个条件成立时进行通知,这里用于通知工作线程有新任务可以执行。
pool->queue_head = NULL;:将任务队列的头指针 queue_head 初始化为 NULL,表示初始时任务队列为空。
pool->busywork = 0;:将忙碌线程计数器 busywork 初始化为 0,表示初始时没有线程在执行任务。
for(int i = 0; i < POOL_NUM; i++) { pthread_create(&pool->tid[i], NULL, workThread, NULL); }:创建了 POOL_NUM 个工作线程,并将它们绑定到执行函数 workThread。
这里使用了 pthread_create() 函数,它会创建一个新线程,将其 ID 存储在 pool->tid[i] 中,并开始执行指定的函数 workThread。由于 workThread 函数没有参数,因此最后一个参数传入了 NULL。
对于pthread_create()
pthread_t thread_id;
:声明一个pthread_t
类型的变量,用于存储新线程的标识符。pthread_create(&thread_id, NULL, thread_function, NULL);
:创建新线程,并3将其标识符存储在thread_id
中。- 第一个参数是指向线程标识符的指针,第二个参数是线程属性,通常我们将其设置为
NULL
表示使用默认属性。第三个参数是一个指向线程函数的指针,即新线程的入口点,最后一个参数是传递给线程函数的参数,这里设置为NULL
。 pthread_join(thread_id, NULL);
:等待新线程结束。主线程会在此处阻塞,直到新线程结束才会继续执行。第一个参数是要等待的线程标识符,第二个参数是一个指针,用于存储线程的返回值,这里设置为NULL
表示不关心线程的返回值。
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *thread_function(void *arg) {
// 在这里编写线程的功能代码
printf("This is a new thread\n");
return NULL;
}
int main() {
pthread_t thread_id; // 定义线程标识符
// 创建新线程
int result = pthread_create(&thread_id, NULL, thread_function, NULL);
if (result != 0) {
printf("线程创建失败\n");
return 1;
}
printf("Main thread: Created thread with ID %ld\n", thread_id);
// 在这里进行其他操作...
// 等待新线程结束
pthread_join(thread_id, NULL);
return 0;
}
3. 线程池添加任务
pool_add_task
{
判断是否有空闲的工作线程
给任务队列添加一个节点
给工作线程发送信号newtask
}
void pool_add_task(int arg)
{
Task *newTask;
pthread_mutex_lock(&pool->taskLock);
while(pool->busywork>=POOL_NUM)
{
pthread_mutex_unlock(&pool->taskLock);
usleep(10000);
pthread_mutex_lock(&pool->taskLock);
}
pthread_mutex_unlock(&pool->taskLock);
newTask = malloc(sizeof(Task));
newTask->func = realwork;
newTask->arg = arg;
pthread_mutex_lock(&pool->taskLock);
Task *member = pool->queue_head;
if(member==NULL)
{
pool->queue_head = newTask;
}
else
{
while(member->next!=NULL)
{
member=member->next;
}
member->next = newTask;
}
pool->busywork++;
pthread_cond_signal(&pool->newTask);
pthread_mutex_unlock(&pool->taskLock);
}
while(pool->busywork>=POOL_NUM)
{
pthread_mutex_unlock(&pool->taskLock);
usleep(10000);
pthread_mutex_lock(&pool->taskLock);
}
1.如果线程池中的忙碌线程数已经达到了最大容量,
就释放线程池的互斥锁,让其他线程有机会去执行任务。
2.等待一段时间,这里是 10 毫秒(通过 usleep(10000) 实现),让其他线程有机会执行,
同时避免线程过于频繁地尝试获取互斥锁。
3.再次尝试获取线程池的互斥锁,以便检查线程池中的忙碌线程数是否已经小于最大容量。
如果线程池中还有可用的空闲线程,函数会释放互斥锁,并创建一个新的任务结构体,并将任务的函数指针和参数赋值给该任务。
然后,函数会再次获取线程池的互斥锁,将新任务添加到任务队列的末尾。接着,函数会增加线程池的忙碌线程数,并通过条件变量通知正在等待的线程有新的任务可以执行。最后,函数释放互斥锁,任务添加完毕。
在代码中,有一段逻辑是在添加任务到任务队列之前再次上锁 pthread_mutex_lock(&pool->taskLock);
这个操作的目的是确保在访问任务队列的过程中线程池的状态保持一致性。
为什么需要再次上锁:
保持线程池状态一致性:在之前的代码中,我们在判断任务队列是否为空并向其中插入新任务时,是在没 有锁定 pool->taskLock 的情况下进行的。
因此,在获取任务队列头部指针 pool->queue_head 并判断是否为空之后,如果其他线程同时访问了任务队列并修改了队列的头部指针,那么当前线程在执行插入操作时可能会出现竞态条件,导致线程池的状态不一致。
防止竞态条件:通过再次上锁 pthread_mutex_lock(&pool->taskLock);
可以防止其他线程在当前线程判断任务队列状态和插入任务之间修改任务队列的操作。这样可以确保当前线程在执行任务队列操作时,其他线程无法干扰,从而保证线程池的状态一致性。
保护临界区:在处理临界资源(例如任务队列)时,必须确保多个线程之间的操作是互斥的。再次上锁可以确保当前线程在访问临界资源时是独占的,避免了多个线程同时访问或修改临界资源的问题。
综上所述,再次上锁的目的是为了保护临界区,在操作任务队列之前确保线程池的状态一致性,并防止竞态条件的发生。
pthread_cond_signal(&pool->newTask):
这是一个条件变量信号操作,用于通知等待在条件变量 pool->newTask 上的线程有新任务可执行。
当新任务被成功添加到任务队列后,我们希望正在等待的工作线程能够及时得知有新任务可用,并尽快执行这些任务。
因此,通过调用 pthread_cond_signal(&pool->newTask),我们向等待的线程发送信号,告知它们有新任务可以执行。
pthread_mutex_unlock(&pool->taskLock):这是解锁线程池的互斥锁 pool->taskLock 的操作。
在添加完新任务并通知等待的线程后,我们不再需要持有互斥锁,
因此可以释放它,以便其他线程能够获取锁并执行任务添加操作。
通过调用 pthread_mutex_unlock(&pool->taskLock),
我们释放了对任务队列的互斥访问,允许其他线程进行任务添加或其他操作。
4. 实现工作线程
workThread
{
while(1){
等待newtask任务信号
从任务队列中删除节点
执行任务
}
}
void *workThread(void *arg)
{
while(1)
{
pthread_mutex_lock(&pool->taskLock);
pthread_cond_wait(&pool->newTask,&pool->taskLock);
Task *ptask = pool->queue_head;
pool->queue_head = pool->queue_head->next;
pthread_mutex_unlock(&pool->taskLock);
ptask->func(ptask->arg);
pool->busywork--;
}
}
-
void *workThread(void *arg)
:这是工作线程函数的定义,它接受一个void*
类型的参数,通常不会使用这个参数,因此在函数体内未使用。 -
while(1)
:这是一个无限循环,表示工作线程会一直处于运行状态,不断地等待 新任务并执行它们。 -
pthread_mutex_lock(&pool->taskLock)
:获取线程池的互斥锁taskLock
,以确保线程安全性。 -
pthread_cond_wait(&pool->newTask, &pool->taskLock)
:线程等待在条件变量pool->newTask
上,一旦收到通知(即有新任务可执行),就会从等待状态返回,并重新获得pool->taskLock
的控制权。这样做是为了避免工作线程持续占用 CPU 时间,而是在有任务可执行时才开始执行任务。int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); -
获取到
pool->taskLock
后,从任务队列中取出头部任务ptask
并更新队列头部指针。 -
释放互斥锁
pthread_mutex_unlock(&pool->taskLock)
,允许其他线程可以继续添加任务或进行其他操作。 -
执行任务函数
ptask->func(ptask->arg)
,即调用任务结构体中保存的函数指针,并传入相应的参数。 -
pool->busywork--
:完成任务后,将线程池中的忙碌线程数减一,表示当前线程空闲。 -
回到循环开始处,继续等待新任务并执行。
这样,工作线程就会循环地等待新任务并执行它们,直到线程池被销毁或程序被终止。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
线程获取互斥锁 mutex,以确保线程安全。
线程调用 pthread_cond_wait() 进入等待状态,同时释放之前获取的互斥锁 mutex。这个过程是原子的,即线程在等待条件变量时会释放互斥锁,并将自己加入到条件变量的等待队列中。
当其他线程调用 pthread_cond_signal() 或 pthread_cond_broadcast() 来通知条件变量时,被阻塞在 pthread_cond_wait() 上的线程会被唤醒。
线程被唤醒后,会重新获取之前释放的互斥锁 mutex,并继续执行。
条件变量知识点:
使用步骤:
初始化:
静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //初始化条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //初始化互斥量
或使用动态初始化
pthread_cond_init(&cond);
生产资源线程:
pthread_mutex_lock(&mutex);
开始产生资源
pthread_cond_sigal(&cond); //通知一个消费线程
或者
pthread_cond_broadcast(&cond); //广播通知多个消费线程
pthread_mutex_unlock(&mutex);
消费者线程:
pthread_mutex_lock(&mutex);
while (如果没有资源){ //防止惊群效应
pthread_cond_wait(&cond, &mutex);
}
有资源了,消费资源
pthread_mutex_unlock(&mutex);
注意:
1 pthread_cond_wait(&cond, &mutex),在没有资源等待是是先unlock 休眠,等资源到了,再lock,所以pthread_cond_wait 和 pthread_mutex_lock 必须配对使用。
2 如果pthread_cond_signal或者pthread_cond_broadcast 早于 pthread_cond_wait ,则有可能会丢失信号。
3 pthead_cond_broadcast 信号会被多个线程收到,这叫线程的惊群效应。所以需要加上判断条件while循环。
5. 线程池的销毁
pool_destory
{
删除任务队列链表所有节点,释放空间
删除所有的互斥锁条件变量
删除线程池,释放空间
}
void pool_destory()
{
Task *head;
while(pool->queue_head!=NULL)
{
head = pool->queue_head;
pool->queue_head = pool->queue_head->next;
free(head);
}
pthread_mutex_destroy(&pool->taskLock);
pthread_cond_destroy(&pool->newTask);
free(pool);
}
6.完整代码
线程池的实现:
1创建线程池的基本结构:
任务队列链表
typedef struct Task;
线程池结构体
typedef struct ThreadPool;
2. 线程池的初始化:
pool_init()
{
创建一个线程池结构
实现任务队列互斥锁和条件变量的初始化
创建n个工作线程
}
3. 线程池添加任务
pool_add_task
{
判断是否有空闲的工作线程
给任务队列添加一个节点
给工作线程发送信号newtask
}
4. 实现工作线程
workThread
{
while(1)
{
等待newtask任务信号
从任务队列中删除节点
执行任务
}
}
5. 线程池的销毁
pool_destory
{
删除任务队列链表所有节点,释放空间
删除所有的互斥锁条件变量
删除线程池,释放空间
}
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define POOL_NUM 10
typedef struct Task
{
void *(*func)(void *arg);
void *arg;
struct Task *next;
}Task;
typedef struct ThreadPool
{
pthread_mutex_t taskLock;
pthread_cond_t newTask;
pthread_t tid[POOL_NUM];
Task *queue_head;
int busywork;
}ThreadPool;
ThreadPool *pool;
void *workThread(void *arg)
{
while(1)
{
pthread_mutex_lock(&pool->taskLock);
pthread_cond_wait(&pool->newTask,&pool->taskLock);
Task *ptask = pool->queue_head;
pool->queue_head = pool->queue_head->next;
pthread_mutex_unlock(&pool->taskLock);
ptask->func(ptask->arg);
pool->busywork--;
}
}
void *realwork(void *arg)
{
printf("Finish work %d\n",(int)arg);
}
void pool_add_task(int arg)
{
Task *newTask;
pthread_mutex_lock(&pool->taskLock);
while(pool->busywork>=POOL_NUM)
{
pthread_mutex_unlock(&pool->taskLock);
usleep(10000);
pthread_mutex_lock(&pool->taskLock);
}
pthread_mutex_unlock(&pool->taskLock);
newTask = malloc(sizeof(Task));
newTask->func = realwork;
newTask->arg = arg;
pthread_mutex_lock(&pool->taskLock);
Task *member = pool->queue_head;
if(member==NULL)
{
pool->queue_head = newTask;
}else
{
while(member->next!=NULL)
{
member=member->next;
}
member->next = newTask;
}
pool->busywork++;
pthread_cond_signal(&pool->newTask);
pthread_mutex_unlock(&pool->taskLock);
}
void pool_init()
{
pool = malloc(sizeof(ThreadPool));
pthread_mutex_init(&pool->taskLock,NULL);
pthread_cond_init(&pool->newTask,NULL);
pool->queue_head = NULL;
pool->busywork=0;
for(int i=0;i<POOL_NUM;i++)
{
pthread_create(&pool->tid[i],NULL,workThread,NULL);
}
}
void pool_destory()
{
Task *head;
while(pool->queue_head!=NULL)
{
head = pool->queue_head;
pool->queue_head = pool->queue_head->next;
free(head);
}
pthread_mutex_destroy(&pool->taskLock);
pthread_cond_destroy(&pool->newTask);
free(pool);
}
int main()
{
pool_init();
sleep(20);
for(int i=1;i<=20;i++)
{
pool_add_task(i);
}
sleep(5);
pool_destory();
}
主函数中首先调用 pool_init()
初始化线程池,然后等待一段时间(20 秒),之后向线程池添加了 20 个任务。接着再等待一段时间(5 秒),最后调用 pool_destory()
销毁线程池。
pthread_mutex_init 是 POSIX 线程(pthread)库中的一个函数,用于初始化互斥锁(mutex),互斥锁用于在多个线程之间同步访问共享资源。以下是 pthread_mutex_init 的原型:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex:指向要初始化的互斥锁对象的指针。
attr:一个可选的指针,指向包含互斥锁初始化属性的互斥锁属性对象。
如果传递了 NULL,则使用默认属性。
pthread_mutex_init 的工作原理简要说明如下:
初始化 mutex 指向的互斥锁。
如果 attr 不是 NULL,则使用 attr 指向的 pthread_mutexattr_t 对象中指定的属性初始化互斥锁属性。
如果 attr 是 NULL,则使用默认属性初始化互斥锁。
初始化完成后,互斥锁可以与其他 pthreads 函数一起使用,
例如 pthread_mutex_lock、pthread_mutex_unlock 和 pthread_mutex_destroy,以实现线程同步,并保护共享资源免受多个线程的并发访问。
&pool->newTask:这是条件变量的地址,表示要初始化的条件变量。
条件变量通常作为数据结构中的一个成员,
这里假设 newTask 是任务池数据结构中的一个条件变量。
NULL:这是一个可选的参数,表示不使用任何条件变量属性,使用默认属性来初始化条件变量。
初始化后,该条件变量就可以被用于线程间的等待和通知操作。
例如,当任务池中有新任务到达时,线程可以通过 pthread_cond_wait 等待条件变量的信号,
而当有新任务到达时,另一个线程可以通过 pthread_cond_signal 或 pthread_cond_broadcast 发送信号通知等待的线程。