8.条件变量和线程池
线程的互斥和同步临界资源概念不能同时访问的资源比如写文件只能由一个线程写同时写会写乱。比如外设打印机打印的时候只能由一个程序使用。外设基本上都是不能共享的资源。生活中比如卫生间同一时间只能由一个人使用。必要性 临界资源不可以共享man手册找不到 pthread_mutex_xxxxxxx 提示No manual entry for pthread_mutex_xxx的解决方法apt-get install manpages-posix-dev互斥锁的创建和销毁两种方法创建互斥锁静态方式和动态方式动态方式int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);其中mutexattr用于指定互斥锁属性如果为NULL则使用缺省属性。静态方式pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;锁的销毁int pthread_mutex_destroy(pthread_mutex_t *mutex)在Linux中互斥锁并不占用任何资源因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外锁定状态则返回EBUSY没有其他动作。互斥锁的使用int pthread_mutex_lock(pthread_mutex_t *mutex)int pthread_mutex_unlock(pthread_mutex_t *mutex)int pthread_mutex_trylock(pthread_mutex_t *mutex)vim 设置代码全文格式化ggG读写锁必要性提高线程执行效率特性写者写者使用写锁如果当前没有读者也没有其他写者写者立即获得写锁否则写者将等待直到没有读者和写者。读者读者使用读锁如果当前没有写者读者立即获得读锁否则读者等待直到没有写者。注意同一时刻只有一个线程可以获得写锁同一时刻可以有多个线程获得读锁。读写锁出于写锁状态时所有试图对读写锁加锁的线程不管是读者试图加读锁还是写者试图加写锁都会被阻塞。读写锁处于读锁状态时有写者试图加写锁时之后的其他线程的读锁请求会被阻塞以避免写者长时间的不写锁初始化一个读写锁 pthread_rwlock_init读锁定读写锁 pthread_rwlock_rdlock非阻塞读锁定 pthread_rwlock_tryrdlock写锁定读写锁 pthread_rwlock_wrlock非阻塞写锁定 pthread_rwlock_trywrlock解锁读写锁 pthread_rwlock_unlock释放读写锁 pthread_rwlock_destroyPOSIX 读写锁默认「写优先」的核心调度规则目的杜绝写线程饥饿。分两段逐一验证1读写锁处于写锁状态写锁是独占模式新来读者申请读锁 → 阻塞新来写者申请写锁 → 阻塞所有加锁请求全部排队阻塞没错。2读写锁处于读锁状态此时出现写锁等待默认写优先策略行为当前已有若干线程拿着读锁第一个写锁请求到来进入等待队列后续所有新来的读锁请求直接阻塞排队不再允许加读锁作用避免读线程源源不断抢占锁让写线程一直得不到执行解决写饥饿问题。完整三态行为速记无锁空闲读锁、写锁都可正常获取持有读锁新来读锁正常加锁多读共享一旦有写锁等待后续读锁全部拦截阻塞优先等写操作持有写锁排他独占读、写全部阻塞等待补充小关键点锁释放顺序写锁释放 → 优先唤醒等待的写线程无写等待才唤醒读线程最后一把读锁释放 → 优先放行排队的写线程策略区别Linuxpthread_rwlock默认就是写优先和你描述一致少数自定义读优先锁才会允许读持续插队容易造成写饥饿。死锁概念避免方法锁越少越好最好使用一把锁调整好锁的顺序 pthread 条件变量 API 详解这张图展示了 POSIX 线程库中条件变量的核心操作函数它们是多线程同步中实现“等待-通知”模型的关键工具。下面为你拆解每个 API 的作用、原理与使用要点。1.pthread_cond_wait无超时等待条件pthread_cond_wait(m_cond,m_mutex);核心作用让当前线程阻塞等待条件变量被唤醒。关键特性调用前必须持有互斥锁m_mutex。函数执行时会原子地释放互斥锁并阻塞线程避免“检查条件”和“进入等待”之间的竞态问题。被唤醒后会重新获取互斥锁再返回。注意事项存在“虚假唤醒”spurious wakeup的可能因此必须在while循环中检查条件而不是if。pthread_mutex_lock(m_mutex);while(!condition){pthread_cond_wait(m_cond,m_mutex);}// 条件满足继续执行pthread_mutex_unlock(m_mutex);2.pthread_cond_timedwait带超时的条件等待intpthread_cond_timedwait(pthread_cond_t*restrict cond,pthread_mutex_t*restrict mutex,conststructtimespec*restrict abstime);核心作用与pthread_cond_wait类似但增加了绝对时间超时限制超时后线程会自动唤醒并返回错误。关键参数abstime超时的绝对时间默认基于CLOCK_REALTIME时钟超时后函数返回ETIMEDOUT。典型场景避免线程无限期阻塞实现“带超时的任务等待”。示例用法structtimespects;clock_gettime(CLOCK_REALTIME,ts);ts.tv_sec5;// 等待最多5秒pthread_mutex_lock(m_mutex);while(!condition){intretpthread_cond_timedwait(m_cond,m_mutex,ts);if(retETIMEDOUT){// 超时处理逻辑break;}}pthread_mutex_unlock(m_mutex);3.pthread_cond_signal唤醒一个等待线程intpthread_cond_signal(pthread_cond_t*cond);核心作用唤醒至少一个等待在该条件变量上的线程。关键特性若没有线程在等待调用后无任何效果。适合“单生产者-单消费者”场景一次唤醒一个线程处理任务减少不必要的竞争。使用建议通常在修改完共享状态后调用无需持有互斥锁也能安全调用但为了避免“丢失信号”建议在持有锁时修改状态并发送信号。4.pthread_cond_broadcast唤醒所有等待线程intpthread_cond_broadcast(pthread_cond_t*cond);核心作用唤醒所有等待在该条件变量上的线程。关键特性会让所有等待线程同时被唤醒它们会重新竞争互斥锁只有一个线程能继续执行其余线程会再次阻塞。典型场景“单生产者-多消费者”场景生产者一次生产多个任务通知所有消费者处理。读写锁的读模式通知写操作完成后通知所有等待的读者线程。使用建议虽然比pthread_cond_signal开销略大但逻辑更安全不会出现“信号丢失”或“线程饿死”的问题不确定时优先使用 broadcast。⚠️ 条件变量的核心使用规范条件必须受互斥锁保护条件变量的状态依赖共享变量必须用互斥锁保证“条件检查”和“状态修改”的原子性。必须用 while 循环检查条件避免虚假唤醒导致的逻辑错误。signal/broadcast 要在状态修改后调用确保线程被唤醒时条件确实已经满足。注意 timedwait 的绝对时间不要直接使用相对时间需基于当前系统时间计算绝对超时时间。完整代码#includestdio.h#includestdlib.h#includepthread.h#includeunistd.h#includetime.h// 缓冲区大小#defineBUFFER_SIZE5// 生产者生产总数#definePRODUCE_COUNT10// 共享缓冲区环形队列intbuffer[BUFFER_SIZE];intin0;// 生产者写入位置intout0;// 消费者读取位置intcount0;// 缓冲区当前元素个数// 同步原语pthread_mutex_tmutexPTHREAD_MUTEX_INITIALIZER;pthread_cond_tnot_fullPTHREAD_COND_INITIALIZER;// 缓冲区非满生产者等待pthread_cond_tnot_emptyPTHREAD_COND_INITIALIZER;// 缓冲区非空消费者等待// 生产者线程函数void*producer(void*arg){intid*(int*)arg;free(arg);for(inti0;iPRODUCE_COUNT;i){// 1. 持有互斥锁检查缓冲区是否已满pthread_mutex_lock(mutex);// 必须用while循环防止虚假唤醒while(countBUFFER_SIZE){printf(生产者%d: 缓冲区已满等待...\n,id);// 2. 等待缓冲区非满信号原子释放锁并阻塞pthread_cond_wait(not_full,mutex);// 被唤醒后会重新持有锁}// 3. 生产数据并放入缓冲区intitemrand()%100;buffer[in]item;in(in1)%BUFFER_SIZE;count;printf(生产者%d: 生产了 %d缓冲区剩余 %d 个\n,id,item,count);// 4. 发送缓冲区非空信号唤醒一个消费者pthread_cond_signal(not_empty);// 5. 释放互斥锁pthread_mutex_unlock(mutex);// 模拟生产耗时usleep(rand()%500000);}printf(生产者%d: 生产完成退出\n,id);returnNULL;}// 消费者线程函数void*consumer(void*arg){intid*(int*)arg;free(arg);while(1){// 1. 持有互斥锁检查缓冲区是否为空pthread_mutex_lock(mutex);// 必须用while循环防止虚假唤醒while(count0){printf(消费者%d: 缓冲区为空等待...\n,id);// 2. 等待缓冲区非空信号原子释放锁并阻塞pthread_cond_wait(not_empty,mutex);// 被唤醒后会重新持有锁}// 3. 从缓冲区取出数据消费intitembuffer[out];out(out1)%BUFFER_SIZE;count--;printf(消费者%d: 消费了 %d缓冲区剩余 %d 个\n,id,item,count);// 4. 发送缓冲区非满信号唤醒一个生产者pthread_cond_signal(not_full);// 5. 释放互斥锁pthread_mutex_unlock(mutex);// 模拟消费耗时usleep(rand()%800000);// 简单退出条件所有生产者都已完成且缓冲区为空// 实际项目中建议使用专门的退出标志if(count0){staticintproducers_done0;if(producers_done2){// 2个生产者printf(消费者%d: 所有任务完成退出\n,id);pthread_mutex_unlock(mutex);break;}}}returnNULL;}intmain(){srand(time(NULL));pthread_tprod1,prod2,cons1,cons2;// 创建2个生产者线程int*id1malloc(sizeof(int));*id11;pthread_create(prod1,NULL,producer,id1);int*id2malloc(sizeof(int));*id22;pthread_create(prod2,NULL,producer,id2);// 创建2个消费者线程int*id3malloc(sizeof(int));*id31;pthread_create(cons1,NULL,consumer,id3);int*id4malloc(sizeof(int));*id42;pthread_create(cons2,NULL,consumer,id4);// 等待所有线程结束pthread_join(prod1,NULL);pthread_join(prod2,NULL);pthread_join(cons1,NULL);pthread_join(cons2,NULL);// 清理资源pthread_mutex_destroy(mutex);pthread_cond_destroy(not_full);pthread_cond_destroy(not_empty);printf(程序正常结束\n);return0;}编译与运行# 编译必须链接pthread库gcc producer_consumer.c-opc-lpthread# 运行./pc关键设计说明双条件变量使用两个独立的条件变量not_full生产者等待缓冲区有空闲位置not_empty消费者等待缓冲区有数据比单条件变量更高效避免不必要的线程唤醒严格的锁保护所有对共享变量buffer、in、out、count的读写都在互斥锁保护下进行条件变量的wait、signal操作也都在持有锁时调用while循环检查条件这是条件变量使用的黄金法则解决了虚假唤醒和多个线程被唤醒后条件再次不满足的问题原子操作保证pthread_cond_wait原子地完成释放锁阻塞线程两个操作避免了检查条件和进入等待之间的竞态条件条件变量应用场景生产者消费者问题是线程同步的一种手段。必要性为了实现等待某个资源让线程休眠。提高运行效率int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);使用步骤初始化静态初始化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 he pthread_mutex_lock 必须配对使用。2 如果pthread_cond_signal或者pthread_cond_broadcast 早于 pthread_cond_wait 则有可能会丢失信号。3 pthead_cond_broadcast 信号会被多个线程收到这叫线程的惊群效应。所以需要加上判断条件while循环。线程池概念和使用概念通俗的讲就是一个线程的池子可以循环的完成任务的一组线程集合必要性我们平时创建一个线程完成某一个任务等待线程的退出。但当需要创建大量的线程时假设T1为创建线程时间T2为在线程任务执行时间T3为线程销毁时间当 T1T3 T2这时候就不划算了使用线程池可以降低频繁创建和销毁线程所带来的开销任务处理时间比较短的时候这个好处非常显著。线程池的基本结构1 任务队列存储需要处理的任务由工作线程来处理这些任务2 线程池工作线程它是任务队列任务的消费者等待新任务的信号线程池的实现1创建线程池的基本结构任务队列链表typedef struct Task;线程池结构体typedef struct ThreadPool;2. 线程池的初始化pool_init(){创建一个线程池结构实现任务队列互斥锁和条件变量的初始化创建n个工作线程}3. 线程池添加任务pool_add_task{判断是否有空闲的工作线程给任务队列添加一个节点给工作线程发送信号newtask}4. 实现工作线程workThread{while(1){等待newtask任务信号从任务队列中删除节点执行任务}}5. 线程池的销毁pool_destory{删除任务队列链表所有节点释放空间删除所有的互斥锁条件变量删除线程池释放空间}编译错误error: ‘ThreadPool {aka struct ThreadPool}’ has no member named ‘head’意义ThreadPool 结构体没有head这个成员。解决检查是否拼写错误。error: too few arguments to function ‘pthread_mutex_init’意思pthread_mutex_init这个函数参数少了解决:检查函数的参数添加对应的参数GDB设置线程锁set scheduler-locking on/offon其他线程会暂停。可以单独调试一个线程