目录
[pthread_cond_t 类型](#pthread_cond_t 类型)
[PTHREAD_COND_INITIALIZER(全局/静态初始化)](#PTHREAD_COND_INITIALIZER(全局/静态初始化))
[pthread_cond_init(动态初始化)](#pthread_cond_init(动态初始化))
[pthread_cond_destroy(变量的销毁)](#pthread_cond_destroy(变量的销毁))
[等待函数 pthread_cond_wait](#等待函数 pthread_cond_wait)
[唤醒函数 pthread_cond_signal/broadcast](#唤醒函数 pthread_cond_signal/broadcast)
[三、相关问题 :](#三、相关问题 :)
上一节课我们学习了线程互斥与互斥锁,今天我们接着学习线程的同步。
一、什么是线程同步
上节课我们认识了互斥,这节课我们来讲一下同步:
引入同步概念
这是我们上一篇文章中抢票代码的运行结果,虽然在抢票代码中我们用互斥锁解决了抢票时出现的负数并发问题,但是仔细观察上面的运行结果我们会发现仍会存在一个线程会连续同时抢很多张票 的场景,这也不合常理,这种场景就是线程缺乏同步性。同步性具体是什么我们下面再讲一个故事帮助我们理解:
我们先讲一个故事 : 学校里有一间VIP自习室,这间自习室空间有限、权限特殊,明确规定每次只能允许一名同学进入自习,而且自习室装着门和锁,必须先拿到唯一的一把钥匙才能开门进去,进去之后还要把门锁上,彻底把门外和里面隔开,以此保证里面的同学不受打扰。这天一早,A 同学第一个跑到自习室门口,他一把抢到了那把唯一的钥匙,立刻打开门冲进自习室,进去后反手把门锁好,还把钥匙紧紧攥在手里。过了一会儿,B、C、D、E 好几个同学陆续赶来,他们都想进自习室自习,可一看门是锁着的,钥匙又被A拿进屋里了,大家只能围在门外干等,有人急得踱步,有人小声抱怨,却一点办法都没有。 就在门外的同学乱作一团地等着时,A 同学突然想上厕所,他拿着钥匙打开门走了出去。要是按最开始的规矩,他出去后应该把钥匙留在门口、把门虚掩着,这样后面的同学就能进去了,可 A 同学偏不,他出去的时候还是把门锁好,钥匙也一直带在身上。等 A 同学上完厕所回来,直接用钥匙开门又进了自习室,继续安安静静自习。门外的B、C、D、E 同学等了好久,还是进不去,A 同学却能随时进出、随时霸占自习室,只要他想回来,不用排队、不用和别人竞争,推门就能进。久而久之,A 同学几乎一直占着自习室,其他同学只能一直干等,甚至等了很久都没机会进去,大家都觉得特别不公平,自习室的使用也完全没规律,要么A一个人一直用,要么门外的人乱哄哄挤成一团,根本没有固定的顺序。后来学校发现了这个问题,专门定了两条规矩:第一条,任何同学从自习室出来、交出钥匙后,要是还想再次进入自习室,必须到门外队伍的末尾重新排队,不能直接插队进去;第二条,所有没拿到钥匙的同学,必须按先来后到的顺序统一排队,不能乱抢钥匙,也不能挤在门口闹事。自从有了这两条规矩,情况就完全不一样了:A 同学出来上厕所后,必须到队伍后面排队,不能直接抢着再进自习室;B、C、D、E 同学按顺序排队,谁先到谁排在前面。轮到 A 时,他进去自习一会儿出来,就乖乖去队尾排队,下一轮轮到他时才能再进去;其他同学也按顺序轮流,大家都能等到机会,既不会有人一直霸占自习室,也不会有人永远没机会,自习室的使用变得公平、有顺序,大家也都能顺利用到自习室。
我们将这个故事对应到我们学习的知识中,就能彻底理解线程、互斥锁、互斥、同步的关系:
-
每一个想去自习室的同学 = **线程(**或进程)
-
VIP自习室 = 共享的临界资源 (同一时刻只能被一个线程访问操作)
-
自习室的那把唯一钥匙 = 互斥锁 (mutex),是保护临界资源的核心凭证
-
每次只能一个人进自习室 = 互斥:互斥锁强制保证同一时刻只有一个线程能持有锁、进入临界区操作资源,这是底线,必须要有;如果没有这把锁,没有互斥约束,多个线程同时进自习室,会把资源搞乱、出现并发问题。
-
门外排队、A 出来要重新排队 =同步:互斥保证了"只能一个人用",但没法保证"谁用、按什么顺序用",容易出现单个线程过度霸占资源的不公平情况;而同步就是在互斥的基础上,给线程的访问加上顺序性规则,比如强制重新排队、按先来后到,让所有线程公平、有规律地轮流使用资源,避免一个线程"为所欲为"。
同步的意义就是让等待的同学顺序统一排队,不能无序争抢,它在互斥已经保证 "同一时刻只有一个线程访问资源" 的基础上,为线程的资源访问增加了明确的顺序性约束,同步不是替代互斥,而是以互斥为前提和基础,解决了纯互斥场景下单个线程过度霸占资源、竞争不合理的问题,通过强制的顺序规则保障了所有线程的公平性,让多线程对共享资源的访问既安全又有序,互斥是线程同步的底线,必须存在,它解决了资源访问的安全性问题,而同步是互斥的延伸与优化,解决了资源访问的公平性与效率问题,二者共同构成了完整的线程同步机制,保障了多线程并发环境下的安全、高效、有序协作。
总结 : 互斥是必须的,它保证每次只有一个线程访问临界资源,守住安全底线;同步是在互斥的前提下,解决公平性和顺序性问题,让多线程访问资源既安全又合理。
同步:在保证数据安全的前提下, 让线程能够按照某种特定的顺序访问临界资源 ,从而有效避免饥饿问题,叫做同步
二、条件变量
引入条件变量
为了实现同步的技术,我们引入一个新概念叫条件变量,这个"条件变量"又是什么我们再讲一个故事:
学校食堂有一个打饭窗口,规定一次只能有一个同学打饭。为了保证不乱,食堂专门放了一个牌子,这个牌子就代表 "使用权"。谁拿到牌子,谁才能去窗口打饭,其他人必须在旁边等着,不能抢。这个牌子,就是锁。 有一天中午,很多同学来打饭,都在抢这个牌子。第一个同学抢到牌子,走到窗口,结果阿姨说:"今天饭还没做好,锅里是空的,打不了饭。"这个同学就很尴尬:他手里拿着锁(牌子),占着窗口,但是条件不满足(没饭),他什么也干不了。他如果一直拿着牌子不走,后面所有同学都别想打饭,整个队伍就会卡死。于是食堂定了一个非常聪明的规则:如果你拿到了牌子,但是发现没饭(条件不满足),你必须把牌子(锁)交出来,然后到旁边专门的等待区坐着等,不能占着牌子。你在等待区安安静静待着,不抢牌子、不挤窗口。等后厨把饭做好了,阿姨大喊一声:"饭来啦!"刚才在等待区坐着的同学,才能重新站起来,去抢牌子,再打饭。
这样一来就非常合理:
- 有牌子才能打饭 → 保证互斥
- 没饭就交出牌子去等 → 不浪费锁
- 饭好了再被叫醒 → 这就是条件变量的通知机制
如果没有这个等待机制:同学会一直拿着牌子反复问 "有饭了吗",牌子永远不放手,后面所有人都卡死,整个食堂彻底堵死。
现在对应到计算机里:
-
每个同学 = 每个线程
-
打饭窗口 = 临界资源
-
那个唯一的牌子 = 互斥锁 mutex
-
拿到牌子 = 加锁 lock
-
交出牌子 = 解锁 unlock
-
保证同一时刻只有一个人使用窗口,就是互斥。
-
饭有没有做好 = 条件变量
8.同学发现没饭,交出牌子,去旁边坐着等 = 线程调用 pthread_cond_wait 它会原子地做两件事:
- 释放锁
- 把自己放入条件变量的等待队列
-
阿姨大喊 "饭来啦" = 另一个线程调用 pthread_cond_signal 和 pthread_cond_broadcast 唤醒等待队列里的线程。
-
等待区 = 条件变量内部维护的等待队列
互斥锁保证了同一时刻只有一个线程能访问资源,但如果线程拿到锁后发现条件不满足(没饭),它不能一直占着锁卡死所有人,于是用条件变量让它释放锁、去等待队列休眠,等条件满足后再被唤醒,重新抢锁执行。条件变量就是为了解决:拿到锁却没法干活的线程,如何优雅等待、不浪费锁的问题。
问题一 : 为什么叫 "条件变量"?因为条件会变吗?
是的,"条件变量" 里的变量,指的就是那个会变的条件,比如有没有饭(0 = 没饭,1 = 有饭)
缓冲区空不空,,信号有没有到来,数据有没有准备好,这个值一开始是 0,后来变成 1,它是会变的,所以叫 "条件变量"。
线程不是在等某个固定东西,而是在等一个会变化的状态,等它从 "不满足" 变成 "满足"。
所以:条件变量 = 等待一个会变化的条件 + 唤醒机制
问题二 : A 把牌子交出后在休息区等,如果饭好了牌子被别人抢到了呢,会不会对 A 不公平?
这个担心非常真实,但是在操作系统里,这就是正常现象,不算不公平。因为A 把牌子交出后在休息区等已经表示 A 自愿放弃锁了,锁就不再属于他了。他选择等待,就等于把使用权完全归还。唤醒只是让大家重新参与竞争,不是 "优先还给 A"。
那这样 A 不就白等了吗?不会白等。因为如果他不交出锁,所有人都卡死,更不公平。唤醒之后,大家重新抢锁,这是操作系统的正常调度规则。**内核一般会按等待时间长短、优先级来唤醒,不会完全乱抢。
就算这次 B 抢到,下次 A 也大概率能抢到,整体是公平的。**谁拿到锁,是锁的竞争规则决定的,不是条件变量决定的。
条件变量的定义
条件变量是用于多线程同步 的一种同步机制,通常与互斥锁配合使用。它允许线程在某个条件不满足时,原子性地释放互斥锁并阻塞等待,直到其他线程(主线程)修改了条件并通过信号唤醒该线程,被唤醒后线程会重新获取互斥锁并继续执行。
核心作用:
- 避免线程在条件不满足时忙等待(空耗 CPU)
- 实现线程间基于条件变化的有序协作
- 必须与互斥锁共同使用,保证条件判断与等待的原子性
条件变量的接口函数
我们先来看看关于条件变量的接口函数
pthread_cond_t 类型
我们在代码中首先需要定义一个 pthread_cond_t 类型的变量,这个变量就是条件变量,然后才能对它进行初始化、等待或唤醒操作。一般锁和这个条件变量是一块定义的。
这是内核源代码里的数据结构,不是用户态我们能看到的东西。我们在代码里用的 pthread_cond _t 是 POSIX 线程库(glibc)给我们封装好的用户态结构体,里面没有队列,只有一些标识、编号、状态位,用来告诉内核 "我在操作哪个条件变量"。真正存放等待线程的队列是 Linux 内核内部的 wait_queue_head_t 之类结构,用户态完全看不见、摸不着。用户态拿着 cond 这个 "凭证",进内核,让内核把当前线程挂到内核里的等待队列里去。
条件变量初始化
PTHREAD_COND_INITIALIZER(全局/静态初始化)
是一个宏,直接赋给变量。直接给 pthread_cond_t 变量赋默认属性。初始化后的条件变量属性都是系统默认的(通常是普通的非共享属性)。适用于全局/静态初始化。大多数日常开发、非特殊需求的场景,都是用这种方式初始化。
pthread_cond_init(动态初始化)
第一个参数 pthread_cond_t *restrict cond 是要初始化的条件变量指针,传入我们定义的 cond 地址。第二个参数 const pthread_condattr_t *restrict attr 是属性指针,用于定制条件变量的特性,一般传 NULL 表示使用默认属性。
pthread_cond_destroy(变量的销毁)
第一个参数 pthread_cond_t *restrict cond 是要初始化的条件变量指针,传入我们定义的 cond 地址,和上面 pthread_cond_init 是配套的。
等待函数 pthread_cond_wait
第一个参数 pthread_cond_t *restrict cond 是要等待的条件变量指针,所有等待这个条件的线程都会挂在这个条件变量对应的内核等待队列上。第二个参数 pthread_mutex_t *restrict mutex 是当前已经持有的互斥锁指针,这是条件变量的灵魂搭档,缺一不可。
核心作用 : 这个函数会原子地、一次性完成两件事,这是条件变量的核心设计:
- 释放手里的互斥锁 mutex:把锁还给系统,让其他线程能抢到锁、修改条件。
- 把当前线程阻塞,挂入 cond 对应的内核等待队列:线程进入休眠,不再占用 CPU,直到被唤醒。
为什么必须原子?
如果先解锁、再等待,中间会有一个极短的时间窗口:
- 线程刚解锁,还没进入等待队列
- 另一个线程刚好抢到锁、修改了条件、发了唤醒信号
- 唤醒信号发完,线程才进入等待队列,就会永远等下去,信号丢失原子操作彻底堵死了这个漏洞,保证解锁和等待是一个不可分割的操作。
唤醒函数 pthread_cond_signal/broadcast
第一个参数 pthread_cond_t *cond 是条件变量指针,函数会操作这个条件变量对应的内核等待队列,唤醒队列里的线程。
pthread_cond_signal 的核心作用是唤醒一个正在该条件变量上等待的线程。Linux 下默认按 FIFO (先来后到)唤醒等待队列的队首线程,也就是最早调用 wait 进入等待的线程。
pthread_cond_broadcast 默认唤醒**所有正在该条件变量上等待的线程。**把条件变量等待队列里的所有阻塞线程全部唤醒,让它们重新参与互斥锁的竞争。
条件变量代码的实现:
我们先来看多个线程同时争抢显示器的代码:
多个线程同时向一个显示器打印也会出现打印错乱的情况,因为显示器没被保护时也是共享资源,所以我们就可以给这条打印语句进行加锁解锁
下面我们给 Print 函数里的打印语句加锁 :
此时加锁之后就不会出现打印错乱的情况了
下面我们引入条件变量 :
运行结果:
下面我们继续添加唤醒函数 pthread_cond_signal :
运行结果:
我们再将 pthread_cond_signal 改为 pthread_cond_broadcast :
运行结果:
三、相关问题 :
问题一 : 为什么 pthread_cond_signal 和 pthread_cond_broadcast 在主线程中,而不是在 pthread_cond_wait 后?
pthread_cond_signal 和 pthread_cond_broadcast 必须放在主线程而非子线程 wait 之后,核心原因在于子线程调用 pthread_cond_wait 后会原子性的释放锁并进入条件变量的等待队列休眠 ,卡在 pthread_cond_wait 这一行永远无法返回,其后续代码完全冻结,如果放在 pthread_cond_wait 则根本没机会执行;而 pthread_cond_signal 和 pthread_cond_broadcast 的本质是由另一个独立线程向等待队列中的线程发送唤醒信号, 子线程无法自己唤醒自己,必须由主线程这个独立的调度实体来执行唤醒操作,主线程创建完所有子线程后,通过循环定时调用 pthread_cond_signal 和 pthread_cond_broadcast,从条件变量的等待队列中逐个唤醒子线程,子线程被唤醒后会重新争抢互斥锁,抢到锁后 pthread_cond_wait 函数才返回,继续执行后续的解锁、循环打印等逻辑,既保证了子线程的循环执行,又完全符合条件变量 "等待 - 唤醒" 的设计规范,避免了子线程永久卡死的问题。
问题二 : pthread_cond_signal 和 pthread_cond_broadcast 的区别
pthread_cond_signal 是一次性从条件变量的等待队列里唤醒一个线程,对应 C++ 标准库的notify_one,代码里主线程循环调用 pthread_cond_signal,就会每次只唤醒 4 个休眠线程中的一个,被唤醒的线程会退出条件队列去争抢互斥锁,抢到锁后 pthread_cond_wait 函数返回,执行后续打印、解锁逻辑,没抢到锁的线程继续在锁阻塞队列排队,最终实现 4 个线程轮流打印的效果;
而 pthread_cond_broadcast 是一次性唤醒条件变量等待队列里的所有线程,对应 C++ 的notify_all,主线程每次唤醒都会把 4 个线程全部喊醒,4 个线程会同时去争抢互斥锁,同一时刻只有一个能抢到锁打印,其余三个会卡在锁阻塞队列,最终 4 个线程会在每次唤醒后轮流打印,和 pthread_cond_signal 的轮流唤醒效果不同,pthread_cond_signal 是逐个唤醒、pthread_cond_broadcast 是全员唤醒后抢锁,二者的核心区别就是唤醒的线程数量不同,底层都是操作条件变量的等待队列,把线程从条件队列转移到锁阻塞队列争抢互斥锁。
问题三 : pthread_cond_signal 和 pthread_cond_broadcast 唤醒线程的过程是给线程发送信号吗
pthread_cond_signal 名字里的 signal,本质就是条件变量提供的唤醒信号机制 ,它的核心作用就是由一个线程(通常是主线程,也可以是其他子线程)向条件变量专属的休息等待队列发送信号,从队列里唤醒一个休眠的线程;线程调用 pthread_cond_wait 进入休息队列后,**只能通过 signal 或 broadcast 这类信号唤醒,无法自行唤醒,**而主线程作为独立的调度实体,不会被子线程的 pthread_cond_wait 阻塞影响,是最适合稳定发送唤醒信号的主体,通过定时循环调用 pthread_cond_signal 和 pthread_cond_broadcast,就能唤醒休息队列里的子线程,让它们从条件队列退出、进入锁阻塞队列抢锁,抢到锁后 pthread_cond_wait 函数返回,继续执行后续代码,完美契合条件变量「等待 - 信号唤醒」的设计逻辑,这也是 signal 名字的由来 ------ 它就是线程间传递唤醒信号的核心接口。
问题四 : 所以条件变量本质内部就是维护了一个队列吗?条件变量内部会做两个操作,一个是把锁打开,另一个操作就是把当时的线程给它放进去这个等待队列里面吗?
条件变量(pthread_cond_t/std::condition_variable)本质上就是一个由操作系统内核维护的专属的条件等待队列数据结构 ,它的核心设计就是为了解决线程间的等待 - 唤醒问题;线程必须在已经持有对应互斥锁的状态下,才能调用 pthread_cond_wait 操作,此时条件变量会原子执行两个不可分割的核心动作:一是自动释放当前持有的互斥锁,二是把当前线程挂入自己内部维护的这个「条件等待队列」中,让线程彻底休眠挂起,不再参与 CPU 调度和锁竞争,直到被其他线程通过 pthread_cond_signal(notify_one)或 pthread_cond_broadcast (notify_all)发送唤醒信号;当收到唤醒信号时,条件变量会从等待队列中取出对应线程,将其转移到互斥锁的阻塞队列中参与锁竞争,只有线程重新抢到锁后, pthread_cond_wait 函数才会返回,线程才能继续执行后续代码,全程严格遵守**「操作共享资源必须持有锁」** 的互斥规则,这就是条件变量的完整本质与核心逻辑。
问题五 : 线程全部同时被唤醒之后,会不会又会出现并发问题?
线程被 pthread_cond_signal / pthread_cond_broadcast 从条件变量的等待队列唤醒后,不会直接并发执行,而是会立刻进入对应互斥锁的阻塞队列中排队抢锁 ,只有抢到锁的线程才能从 pthread_cond_wait 返回、继续执行临界区代码,没抢到锁的线程会一直阻塞在锁的阻塞队列中,同一时刻永远只有一个线程能持有锁进入临界区,从根本上杜绝了并发问题;哪怕用 pthread_cond_broadcast 一次性唤醒所有等待线程,它们也只会全部进入锁的阻塞队列争抢互斥锁,不会出现多个线程同时操作共享资源的情况,全程严格遵循互斥锁的保护规则,这就是条件变量配合互斥锁实现安全同步的核心保障。
问题六 : 锁需要被争抢吗?谁能抢到锁是操作系统调度器负责的吗?
锁必须抢,但是谁能抢到锁,不完全由调度器说了算。
锁确实要抢,当多个线程同时执行 pthread_mutex_lock 时,大家都会先用 CPU 原子指令 (swap/exchange)去改锁的状态变量。只有一个线程能抢成功,把锁的状态从 0 改成 1,其他的全部失败。这一步是硬件级别的并发竞争,不是谁安排的,就是纯硬件原子操作谁先撞上谁赢。
抢失败的线程,才会交给调度器,这些线程进入锁的阻塞队列,然后主动让出 CPU,告诉操作系统:我睡了,别调度我。这一步才进入操作系统调度器的管理范围。解锁时,唤醒谁是调度器决定的,但不完全随机,当持有锁的线程 pthread_mutex_ulock 时,内核会从锁的阻塞队列里唤醒一个或多个线程。唤醒哪个,一般遵循先来先服务 (FIFO) 或按线程优先级,
这是调度器的策略,不是完全乱抢。
问题七 : 直接抢锁失败进入的阻塞队列,和从条件变量休息队列被唤醒后进入的阻塞队列,是同一个队列吗?
不管是初始抢锁失败,还是被条件变量唤醒后去抢锁,线程进入的都是同一个锁阻塞队列,条件变量的等待队列只是临时休眠的地方,最终等锁都在互斥锁的这一个阻塞队列里排队。
问题八 : pthread_mutex_t 互斥锁的「锁阻塞队列」和 pthread_cond_t 条件变量的「条件等待队列」在它们的底层都存在吗?
是的,两个队列在底层都是真实存在、由操作系统内核实现的,是实实在在的数据结构。
1. pthread_mutex_t 互斥锁的「锁阻塞队列」
- 底层归属:Linux 内核中,每一把 pthread_mutex_t 互斥锁,内核都会为它维护一个等待队列(wait queue),本质就是一个双向链表。
- 作用:当线程调用 pthread_mutex_lock 抢锁失败时,内核会把这个线程的 task_struct (进程控制块,线程的内核描述符)挂到这个链表上,线程进入阻塞状态,不再参与 CPU 调度。
- 唤醒逻辑:当持有锁的线程调用 pthread_mutex_unlock 时,内核会从这个链表的头部取出一个线程,把它唤醒,让它去抢锁。
- 对应我们的代码:4 个线程抢 gmutex 锁,没抢到的,就被内核挂到 gmutex 对应的阻塞队列链表上,卡在 pthread_mutex_lock 这一行。
2. pthread_cond_t 条件变量的「条件等待队列」
- 底层归属:每一个 pthread_cond_t 条件变量,内核同样会为它维护一个专属的等待队列(也是双向链表),和互斥锁的队列完全独立。
- 作用:当线程调用 pthread_cond_wait 时,内核会原子完成两件事:把线程从 gmutex 的阻塞队列中移除、释放锁,然后把线程的 task_struct 挂到 gcond 对应的条件等待队列链表上,线程彻底休眠,不参与 CPU 调度。
- 唤醒逻辑:当其他线程调用 pthread_cond_signal 或 pthread_cond_broadcast 时,内核会从 gcond 的条件等待队列中,把线程取出来,重新放到 gmutex 的锁阻塞队列里,让它们去抢锁。
- 对应我们代码:4 个线程执行 pthread_cond_wait 后,全部被内核挂到 gcond 的条件等待队列链表上,永久休眠,直到被唤醒。
需要注意的是这两个队列不是在 pthread 库的用户态代码里实现的,是 Linux 内核 futex (快速用户空间互斥锁)机制的一部分, 由内核全权管理,线程的阻塞、唤醒、队列操作都是内核级的原子操作,不会被用户态线程切换打断。
两个队列都是内核中的 wait_queue_head_t 类型的链表头,线程的 task_struct 通过 wait_queue_t 节点挂到链表上,内核通过遍历链表完成唤醒、调度。我们之所以感知不到是因为这些队列是内核维护的,用户态代码看不到链表结构,只能通过 pthread 接口间接操作,所以我们会误以为只是逻辑概念,但它们是实实在在存在的内存数据结构。
如果我们想看这两个队列的底层实现其实在 VS Code 里,是无法通过"转到定义"看到这两个真实的队列,因为它们不在用户态头文件里,而是藏在 Linux 内核内部,用户态代码完全看不到。所以我们只能在它 Linux 内核源码里查看。
内核源码里大致是这样的
用户态快速路径和内核态慢速路径
问题八 : 我们在上一篇文章中讲解 pthread_mutex_lock 加锁,它的内部是用 al/eax 寄存器做原子比较交换,那和我们现在说的互斥锁中的阻塞队列有关系吗?
我们上一篇讲解的 pthread_mutex_lock 底层用 al/eax 寄存器做原子比较交换(swap/xchg 这类原子指令),是用户态快速路径(fast path)的实现,对应 Linux 的 futex 机制:当锁处于空闲状态时,线程直接通过用户态原子指令抢锁成功,不涉及内核、不涉及队列,全程在用户态完成,效率极高;
**只有当锁已经被其他线程持有、抢锁失败时,才会触发内核态慢速路径(slow path),**此时操作系统内核会为这把互斥锁维护一个专属的锁阻塞队列 (本质是内核双向链表),把抢锁失败的线程挂入这个队列中彻底阻塞,不再参与 CPU 调度,直到持有锁的线程调用 unlock 时,内核会从阻塞队列中取出线程唤醒,让其重新参与锁竞争;而条件变量的条件等待队列是完全独立的另一套内核队列,仅服务于 pthread_cond_wait 操作,
二者是同一把锁的用户态快速路径和内核态慢速路径,共同构成了互斥锁的完整底层实现,不存在矛盾,只是我们之前只了解了用户态的快速路径,没深入到内核态的阻塞队列实现而已。
问题九 : 那上一篇抢票的代码也是四个线程同时抢锁访问资源呀,那同时抢的话怎么可能会出现锁是空闲的情况呢?而今天的四个线程同时被一次性唤醒了,会出现抢锁的情况并涉及到阻塞队列我能理解。
首先,上一篇抢票的代码中的 4 线程场景和今天的 4 线程场景,本质都是多线程并发抢锁, 只是触发阻塞队列的时机不同:上一篇抢票的代码中,4 个线程启动后就会并发抢锁,第一个抢到锁的线程会立刻用用户态原子指令(swap/exchange)把锁置为占用,后续 3 个线程抢锁失败,触发内核态慢速路径,被挂入互斥锁的阻塞队列排队,直到持有锁的线程解锁,内核唤醒队列中的线程继续抢锁,只是线程拿到锁后会直接执行完临界区、解锁、退出,没有 pthread_cond_wait 操作,所以我们只看到了用户态原子指令的快速抢锁,没感知到内核阻塞队列的存在;
而今天的场景中,4 个线程先轮流拿到锁打印、执行 pthread_cond_wait 后原子释放锁、进入条件变量的等待队列休眠,当主线程调用pthread_cond_broadcast 一次性唤醒所有线程时,4 个线程会同时从条件等待队列退出,进入互斥锁的阻塞队列争抢锁,**此时锁大概率处于被主线程或其他线程持有的状态,4 个线程会全部触发内核态慢速路径,挂入阻塞队列排队,**只有抢到锁的线程才能从 pthread_cond_wait 返回、执行后续代码,没抢到的继续阻塞,这就直观体现了互斥锁的阻塞队列机制;哪怕是pthread_cond_signal 逐个唤醒的场景,被唤醒的线程也会进入阻塞队列抢锁,只是每次只唤醒一个,队列中排队的线程数量更少,本质上所有多线程并发抢锁的场景,只要锁不空闲,就一定会触发内核阻塞队列,用户态原子指令只负责锁空闲时的快速抢锁,内核阻塞队列负责处理所有抢锁失败的阻塞场景,二者共同保障了互斥锁的安全同步,昨天的场景只是因为线程执行快、解锁快,队列的存在不明显,今天的唤醒后抢锁场景则让阻塞队列的作用完全凸显了出来。
问题十 : 那可不可以理解为四个线程,线程1抢到了锁,并加锁,它在用户层面上执行 al 与锁状态值的交换判断,加锁成功后,其他的3个线程通过 al 与锁状态值判断不相符,所以这3个线程就在内核层面上被放入一个阻塞队列里了。先用户后内核吗?
完全正确,在现代操作系统中,为了极致性能,常见的用户态互斥锁通常会采用**"先用户态原子尝试,失败再陷入内核"** 的优化策略。流程是:用户态原子指令尝试加锁 → 成功就直接走 → 失败后才进入内核态阻塞队列。当用户态原子指令失败后进入内核态的这个过程,pthread 库会执行:
bashfutex(WORLD, FUTEX_WAIT, ...)这是一个 轻量级系统操作,不是完整的系统调用,而是内核提供的 futex 快速接口。
它做两件事:
- 把当前线程加入阻塞队列(锁的等待队列)
- 把线程从 RUNNING 状态改为 TASK_INTERRUPTIBLE,让出 CPU
这个切换不是中断,也不是系统调用,而是 futex(快速用户态互斥锁) 特有的内核态进入方式:futex 是用户态主动请求进入内核态处理等待队列的特殊机制。
四个线程并发抢锁时,一定先执行用户态原子指令(swap/exchange),用 al寄存器与锁状态值做交换判断;线程 1 判断成功、抢到锁并加锁,直接进入临界区执行;剩下 3 个线程判断失败、抢锁失败,才会从用户态进入内核态,被操作系统内核挂入这把互斥锁专属的阻塞队列中休眠排队。
和今天的代码场景本质完全一致,都是先用户态抢锁、失败后进入内核态队列,唯一区别仅在于:上一篇抢票的线程启动后直接抢锁,因执行快、解锁快,内核阻塞队列不明显;今天线程先通过 prhtead_cond_wait 进入条件变量休息队列休眠,被 signal/broadcast 唤醒后,全部重新进入同一把互斥锁的内核阻塞队列抢锁,让内核队列的作用被清晰看到;无论线程来自初始抢锁,还是从条件等待队列唤醒,最终排队抢锁的,都是同一个互斥锁内核阻塞队列,用户态只负责快速抢锁,内核态只负责阻塞排队,二者层级分明、永不混乱。
问题11 : 所以抢资源就是抢锁?
对,抢资源 = 抢锁 ,这句话基本可以直接成立。我们说的 "抢资源",本质上就是抢那把互斥锁 。因为共享资源(比如 stdout 打印、全局变量、缓冲区)本身不能直接 "抢",操作系统是靠锁 来保护它们的。谁抢到锁,谁就有权进临界区、操作共享资源;没抢到锁的线程,就会被内核放进锁的阻塞队列里等着。所以线程之间争打印、争变量、争缓冲区,最终表现出来的,全都是在抢这把 mutex 锁。不管是一开始直接抢锁,还是从条件变量休息队列被唤醒后再去抢锁,大家抢的都是同一个锁、进的也是同一个阻塞队列,调度器和原子指令决定谁先抢到,抢到就等于拿到了操作共享资源的权限。
四、总结
本文深入讲解了线程同步机制,重点分析了条件变量的工作原理及其与互斥锁的配合关系。文章通过生动的生活案例(VIP自习室、食堂打饭窗口)类比线程同步问题,形象解释了互斥锁保证资源独占访问、条件变量解决等待唤醒的核心机制。详细剖析了条件变量的关键操作:pthread_cond_wait会原子性地释放锁并进入等待队列;pthread_cond_signal/broadcast负责唤醒等待线程。特别强调了条件变量必须与互斥锁配合使用,以及线程唤醒后仍需重新抢锁才能继续执行的同步逻辑。通过代码示例展示了条件变量的实际应用,并解答了关于线程唤醒顺序、队列管理等常见疑问,完整呈现了多线程同步的安全实现方案。
谢谢大家的观看!





第一个参数 pthread_cond_t *restrict cond 是要初始化的条件变量指针,传入我们定义的 cond 地址,和上面 pthread_cond_init 是配套的。











