并发机制¶
约 8908 个字 636 行代码 8 张图片 预计阅读时间 38 分钟




volatile关键字¶
volatile 是 C/C++ 中的核心关键字,核心作用是**告诉编译器:被修饰的变量是 “易变的”,禁止对其做任何优化(如缓存到寄存器、指令重排),必须每次都直接读写内存**—— 它是保证内存可见性的基础,尤其在并发编程、硬件交互、信号处理等场景中不可或缺。
核心特性¶
volatile 对变量的约束可总结为 3 个 “禁止”,直接决定了它的生效范围:
- 禁止寄存器缓存:编译器不能将变量值缓存到 CPU 寄存器中,必须每次读写都直接访问内存(主存 / 硬件寄存器);
- 禁止指令重排:编译器不能重排涉及该变量的内存操作(仅针对该变量,而非所有内存操作);
- 禁止优化删除:编译器不能因为 “看似未使用” 而删除对该变量的读写操作(如硬件寄存器的写操作)。
与普通变量的对比(直观示例)
// 普通变量:编译器会优化为“寄存器缓存”
int normal_var = 0;
void test_normal() {
normal_var = 1; // 写入寄存器
normal_var = 2; // 编译器会优化为“直接写2”,删除前一行
int a = normal_var; // 从寄存器读取,而非内存
}
// volatile变量:禁止所有优化
volatile int vol_var = 0;
void test_volatile() {
vol_var = 1; // 直接写入内存,不缓存
vol_var = 2; // 编译器不会删除前一行,两次写操作都保留
int a = vol_var; // 直接从内存读取,而非寄存器
}
生效范围与边界¶
1、 生效范围(核心)
- 作用对象:仅修饰**变量**(全局 / 局部 / 指针指向的内存),不能修饰函数 / 常量;
- 约束层级:仅作用于**编译器**(编译期),不生成任何 CPU 指令,对 CPU 执行无影响;
- 覆盖范围:仅针对**被修饰的单个变量**,不影响其他变量的优化;
- 不涉及:CPU 指令重排、多 CPU 缓存一致性、原子性(这是常见误区)。
2、关键边界(哪些场景不生效)
| 场景 | volatile 是否生效 | 补充说明 |
|---|---|---|
| 编译器缓存变量到寄存器 | ✅ 禁止 | 核心作用 |
| 编译器重排该变量的读写 | ✅ 禁止 | 仅针对该变量,其他变量仍可能重排 |
| CPU 重排该变量的指令 | ❌ 不禁止 | 需硬件内存屏障(如 mfence) |
| 多 CPU 缓存一致性 | ❌ 不保证 | 需硬件屏障 + 缓存协议(如 MESI) |
| 多线程操作的原子性 | ❌ 不保证 | 需原子操作(如 CAS)/ 锁 |
典型使用场景¶
1、并发编程:多线程共享变量(保证可见性)
多线程共享变量时,volatile 保证线程能读到其他线程修改后的最新值(编译器不缓存):
// 全局共享标志位:必须加volatile
volatile int flag = 0;
// 线程1:修改标志位
void thread1() {
flag = 1; // 直接写内存,不缓存到寄存器
}
// 线程2:读取标志位
void thread2() {
while (flag == 0) { // 每次都从内存读,能及时看到thread1的修改
// 自旋等待
}
printf("flag is set\n");
}
⚠️ 注意:volatile 仅保证可见性,不保证原子性!如下代码仍有并发问题:
volatile int count = 0;
// 多线程执行:count++ 是“读-改-写”三步,非原子操作,仍会竞态
void inc() {
count++; // volatile 无法保证这一步的原子性!
}
2、硬件交互:访问内存映射寄存器
嵌入式 / 驱动开发中,硬件寄存器通常映射到内存地址,volatile 保证读写操作不被编译器优化:
// 硬件 UART 数据寄存器(内存地址 0x1000)
#define UART_DATA (*(volatile unsigned char *)0x1000)
void uart_send(char c) {
// 写操作:必须直接写入硬件寄存器,不能被编译器优化删除
UART_DATA = c;
// 读操作:必须等待硬件置位,每次读内存(寄存器状态可能被硬件修改)
while (volatile UART_STATUS & 0x01); // 等待发送完成
}
3、信号处理:信号处理函数与主函数共享变量
信号处理函数异步修改变量时,volatile 保证主函数能读到最新值:
volatile sig_atomic_t signal_received = 0;
void signal_handler(int sig) {
signal_received = 1; // 信号处理函数异步修改
}
int main() {
signal(SIGINT, signal_handler);
// 主函数循环读取:每次从内存读,能及时看到信号触发
while (!signal_received) {
// 业务逻辑
}
printf("Signal received\n");
return 0;
}
volatile 与内存屏障的区别¶
很多人会混淆 volatile 和内存屏障,二者互补但核心作用完全不同:
| 特性 | volatile | 编译期内存屏障(__asm__("" ::: "memory")) | 硬件内存屏障(mfence) |
|---|---|---|---|
| 约束对象 | 单个变量的编译器优化 | 所有内存操作的编译器优化 | CPU 指令执行 / 缓存 |
| 核心作用 | 禁止寄存器缓存、单变量重排 | 禁止所有内存操作重排 | 禁止 CPU 重排、刷新缓存 |
| 原子性 | 不保证 | 不保证 | 不保证(需原子指令) |
| 多 CPU 可见性 | 不保证 | 不保证 | 保证 |
互补使用示例(并发场景)
volatile int flag = 0;
int data = 0;
void producer() {
data = 100;
// 编译期屏障:禁止编译器重排data和flag的写操作
__asm__ volatile ("" ::: "memory");
// 硬件屏障:禁止CPU重排,刷新缓存(多CPU场景)
__asm__ volatile ("mfence" ::: "memory");
flag = 1; // volatile 保证写内存,不缓存
}
void consumer() {
while (flag == 0); // volatile 保证每次读内存
printf("data = %d\n", data); // 保证读到100
}
常见误区¶
误区 1:volatile 能保证原子性
❌ 错误:volatile 仅保证 “每次读写内存”,但无法保证 “读 - 改 - 写” 操作的原子性(如 count++);
✅ 正确:原子性需依赖 CAS(如 __sync_bool_compare_and_swap)、锁或原子指令。
误区 2:volatile 能禁止 CPU 重排
❌ 错误:volatile 仅约束编译器,CPU 仍会重排指令;
✅ 正确:需搭配硬件内存屏障(如 mfence)禁止 CPU 重排。
误区 3:所有共享变量都要加 volatile
❌ 错误:volatile 会禁用编译器优化,降低性能;仅在 “变量可能被异步修改(线程 / 信号 / 硬件)” 时使用。
误区 4:volatile 能替代内存屏障
❌ 错误:volatile 仅针对单个变量,内存屏障约束所有内存操作的顺序;
✅ 正确:并发场景需 “volatile + 内存屏障” 配合。
总结¶
volatile是**编译器约束关键字**,核心禁止对单个变量的寄存器缓存、指令重排、优化删除;- 仅保证**内存可见性**,不保证原子性、CPU 重排、多 CPU 缓存一致性;
- 典型场景:多线程共享变量、硬件寄存器访问、信号处理;
- 与内存屏障互补使用,而非替代 ——
volatile管 “编译器不偷懒”,内存屏障管 “CPU 不捣乱”。
简单说:volatile 就像给变量贴了个 “易碎品” 标签,告诉编译器:“这个变量随时可能被修改,你别瞎优化,每次都要老老实实去内存里读写,不准偷懒用寄存器!”。
内存屏障¶
为什么需要这个内存屏障?¶
编译器为了优化性能,会对内存操作做 “不合理” 的重排或缓存,导致并发场景下逻辑错误。我们通过对比示例理解:
反例:编译器优化导致的内存操作重排
编译器可能生成如下汇编(重排后):
正例:内存屏障禁止重排
编译器必须保证:a=1 的写操作在屏障前执行,b=2 的写操作在屏障后执行,无法重排。
另一个核心场景:禁止寄存器缓存内存值
volatile int flag = 0;
int data = 0;
void producer() {
data = 100; // 写数据
__asm__ volatile ("" ::: "memory"); // 屏障:强制写回主存
flag = 1; // 置位标志位
}
void consumer() {
while (flag == 0); // 等待标志位
printf("data = %d\n", data); // 保证读到最新的 100
}
- 若无内存屏障,编译器可能将
data=100缓存到寄存器,未及时写回主存; - 此时
flag=1先被消费者看到,但消费者读取data时可能拿到旧值(0),导致逻辑错误; - 内存屏障强制
data=100写回主存,保证消费者能读到最新值。
编译期内存屏障¶
__asm__ volatile ("" ::: "memory") 是 GCC 内嵌汇编的**空指令内存屏障**,核心作用是**禁止编译器对内存读写操作进行重排和优化**,强制保证内存操作的顺序性和可见性 —— 这是底层并发编程(如原子操作、锁、内核代码)中保证正确性的关键手段,也是 Nginx、Linux 内核等高性能代码的常用技巧。
1、 GCC 内嵌汇编的标准格式:
| 部分 | 含义 |
|---|---|
__asm__ | 声明内嵌汇编(等价于 asm,双下划线是 GNU 风格); |
volatile | 禁止编译器优化 / 删除该汇编块(即使指令为空); |
"" | 空的汇编指令(无实际 CPU 指令执行); |
::: | 无输出约束、无输入约束(前两个冒号为空); |
"memory" | 破坏描述符(Clobber List),核心作用是告诉编译器:该汇编块 “修改了内存”,需强制刷新缓存、禁止内存操作重排。 |
2、核心本质
这是一个**编译期内存屏障(Compiler Barrier)**,仅作用于编译器优化阶段,不生成任何 CPU 指令,但会强制编译器遵守以下规则:
- 禁止指令重排:屏障前的所有内存读写操作,不能被编译器重排到屏障后;屏障后的内存读写操作,也不能被重排到屏障前;
- 强制缓存刷新:编译器必须将寄存器中缓存的内存值写回主存,且后续读取必须从主存重新加载,不能复用寄存器中的旧值;
- 不影响 CPU 执行:无硬件指令,CPU 层面的指令重排 / 缓存一致性仍需硬件内存屏障(如 x86 的
mfence)保证。
硬件内存屏障¶
"memory" 屏障的关键特性
(1)仅作用于编译器,不影响 CPU
- 编译期:编译器生成汇编时,严格遵守内存操作顺序,不缓存内存值;
- 运行期:无任何 CPU 指令执行,CPU 仍可能做指令重排(需硬件屏障补充)。
(2)与硬件内存屏障的区别(x86 为例)
| 类型 | 示例 | 作用范围 | 核心目的 |
|---|---|---|---|
| 编译期屏障(Compiler Barrier) | __asm__ volatile ("" ::: "memory") | 仅编译器 | 禁止编译器重排 / 缓存内存操作 |
| 硬件屏障(Hardware Barrier) | asm volatile ("mfence" ::: "memory") | CPU 层面 | 禁止 CPU 重排指令、刷新缓存一致性 |
(3)常见组合用法(兼顾编译 + 硬件屏障)
在并发场景中,通常需要同时禁止编译器和 CPU 重排,示例:
// x86 架构:mfence 是硬件内存屏障,搭配 memory 编译屏障
__asm__ volatile ("mfence" ::: "memory");
// ARM 架构:dmb sy 是硬件屏障
__asm__ volatile ("dmb sy" ::: "memory");
硬件内存屏障(Hardware Memory Barrier/Fence)是**CPU 提供的专用指令**,核心作用是强制约束 CPU 的指令执行顺序、刷新缓存一致性,解决多核心 / 多 CPU 场景下的内存可见性和指令重排问题 —— 它是底层并发编程(如原子操作、无锁算法、内核调度)的基石,生效范围覆盖 CPU 核心的指令流水线、缓存系统和内存总线。
1、为什么需要硬件屏障?
现代 CPU 为了提升性能,会做两类 “优化”,但在多核心场景下会破坏内存操作的正确性:
- 指令重排:CPU 乱序执行指令(如先执行写操作 B,再执行写操作 A),导致内存操作顺序与代码逻辑不一致;
- 缓存延迟:CPU 修改数据后先存在本地缓存(L1/L2),不立即刷到主存,其他核心无法及时看到修改。
硬件内存屏障的核心就是**禁止这些优化**,强制 CPU 按 “代码逻辑顺序” 执行内存操作,并保证修改对其他核心可见。
| 特性 | 硬件内存屏障 | 编译期内存屏障(如__asm__("" ::: "memory")) |
|---|---|---|
| 约束对象 | CPU 硬件(指令流水线、缓存、总线) | 编译器(仅约束代码编译阶段的优化) |
| 生成指令 | 有(如mfence/dmb) | 无(空指令,仅约束编译器) |
| 生效范围 | 执行该指令的 CPU 核心(跨线程 / 进程) | 单个编译单元(.c 文件) |
| 解决问题 | CPU 指令重排、多核心缓存一致性 | 编译器指令重排、寄存器缓存 |
2、硬件内存屏障的分类(按操作类型)
硬件屏障按约束的内存操作类型,分为三类(不同架构指令不同,但逻辑一致):
(1)读屏障(Load Fence / LFENCE)
- 核心作用:约束**内存读取(Load)** 操作,禁止 CPU 将 “屏障后的读操作” 重排到 “屏障前的读操作” 之前;
- 保证:屏障前的所有读操作完成后,才执行屏障后的读操作;
-
典型指令:
-
x86/x86_64:
lfence -
ARM64:
dmb ld(数据内存屏障 - 读) -
RISC-V:
fence r, rw
-
示例(x86 读屏障):
int a = 0, b = 0;
// 线程1
void thread1() {
a = 1;
b = 2;
}
// 线程2(CPU1)
void thread2() {
int val_b = b; // 读操作1
lfence; // 读屏障
int val_a = a; // 读操作2
// 屏障保证:val_b的读取一定在val_a之前完成,不会被CPU重排
}
(2)写屏障(Store Fence / SFENCE)
- 核心作用:约束**内存写入(Store)** 操作,禁止 CPU 将 “屏障后的写操作” 重排到 “屏障前的写操作” 之前;
- 保证:屏障前的所有写操作刷新到缓存 / 主存后,才执行屏障后的写操作;
-
典型指令:
-
x86/x86_64:
sfence -
ARM64:
dmb st(数据内存屏障 - 写) -
RISC-V:
fence rw, w
-
示例(x86 写屏障):
int data = 0, flag = 0;
// 线程1(CPU0)
void thread1() {
data = 100; // 写操作1
sfence; // 写屏障:强制data写入缓存/主存
flag = 1; // 写操作2:保证在data之后执行
}
// 线程2(CPU1)
void thread2() {
while (flag == 0);
printf("data = %d\n", data); // 能读到最新的100,而非缓存旧值
}
(3)全屏障(Full Fence / MFENCE)
- 核心作用:约束**所有内存读写操作**,禁止 CPU 将屏障前的任何内存操作(读 / 写)重排到屏障后,反之亦然;
- 保证:屏障前的所有内存操作完成并刷新到主存后,才执行屏障后的操作;
-
典型指令:
-
x86/x86_64:
mfence/lock前缀(隐含全屏障) -
ARM64:
dmb sy(数据内存屏障 - 系统级) -
RISC-V:
fence rw, rw
-
示例(x86 全屏障):
3、主流架构的硬件屏障指令对比
不同 CPU 架构的内存模型差异(如 x86 是 “强内存模型”,ARM 是 “弱内存模型”),导致硬件屏障的指令和必要性不同:
| 架构 | 内存模型 | 核心屏障指令 | 关键特点 |
|---|---|---|---|
| x86/x86_64 | 强内存模型 | lfence/sfence/mfence | 天然禁止大部分写重排,仅特殊场景需显式屏障;lock前缀(如lock cmpxchg)隐含全屏障 |
| ARM64 | 弱内存模型 | dmb/dsb/isb | 需显式加屏障保证顺序;dmb sy是最常用的系统级全屏障 |
| RISC-V | 弱内存模型 | fence | 可灵活指定约束的操作类型(读 / 写) |
| PowerPC | 弱内存模型 | lwsync/sync | sync是全屏障,lwsync是轻量级屏障 |
4、关键补充:x86 的 “隐式屏障”
x86 架构的lock前缀(如原子操作lock cmpxchgl)会自动触发以下效果,等价于全屏障:
- 锁定内存总线,保证操作原子性;
- 禁止 CPU 重排该指令前后的内存操作;
- 强制刷新缓存到主存。 这也是 Nginx 的
ngx_atomic_cmp_set中仅用lock前缀,无需额外mfence的原因。
5、硬件屏障的生效机制(底层原理)
硬件屏障通过以下 3 种方式保证内存操作的顺序和可见性:
- 阻塞指令流水线:CPU 执行屏障指令时,会暂停指令流水线,直到屏障前的所有内存操作(读 / 写)完成,才继续执行后续指令 —— 禁止指令重排。
-
刷新缓存一致性:
-
写屏障 / 全屏障会强制将 CPU 本地缓存(L1/L2)中的修改刷到共享缓存(L3)或主存;
-
触发 MESI 缓存一致性协议,让其他 CPU 核心的无效缓存行失效,必须重新从主存加载最新值。
-
-
锁定内存总线(部分指令):如 x86 的
lock前缀,会独占内存总线,禁止其他 CPU 核心在该指令执行期间访问内存,保证原子性和顺序性。
6、常见误区
误区 1:x86 架构不需要硬件屏障
❌ 错误:x86 是强内存模型,禁止 “写→读”“写→写” 重排,但 “读→写”“读→读” 仍可能重排(如 CPU 预取),特殊场景(如 IO 映射、多线程共享)仍需lfence/mfence。
误区 2:硬件屏障能跨 CPU 生效
❌ 错误:硬件屏障仅对**执行该指令的 CPU 核心**生效;其他核心需通过缓存一致性协议(如 MESI)看到修改,或自己执行屏障。
误区 3:硬件屏障越多越好
❌ 错误:硬件屏障会阻塞 CPU 流水线、刷新缓存,频繁使用会降低性能(x86mfence约 5~10 纳秒 / 次),需按需使用。
7、关键点回顾
- 硬件内存屏障是 CPU 指令,核心约束**指令执行顺序**和**缓存一致性**,解决多核心下的内存可见性问题;
- 分三类:读屏障(约束读)、写屏障(约束写)、全屏障(约束读写),不同架构指令不同;
- x86 是强内存模型,
lock前缀隐含全屏障;ARM/RISC-V 是弱内存模型,需显式加屏障; - 生效范围是 “执行该指令的 CPU 核心”,跨核心一致性依赖缓存协议 + 屏障配合。
简单说:硬件内存屏障是给 CPU 的 “强制命令”——“停下你的乱序执行和缓存偷懒,按我写的顺序执行内存操作,并且把修改同步给其他核心!”。
生效范围¶
内存屏障的本质是 “顺序约束”:它约束的是「屏障指令前后的内存操作」,而非 “函数内 / 外”。无论屏障写在函数内还是函数外,只要内存操作跨越了屏障,就会被约束 —— 函数只是代码组织的形式,不影响屏障的核心作用范围。
举个直观例子(编译期屏障):
// 全局共享变量
int a = 0, b = 0;
// 函数内的内存屏障
void set_a() {
a = 1;
// 函数内的编译期屏障
__asm__ volatile ("" ::: "memory");
}
void set_b() {
// 屏障在 set_a() 内,但仍约束 a=1 和 b=2 的顺序
set_a();
b = 2;
}
- 虽然屏障写在
set_a()函数内部,但它依然能禁止编译器将b=2(set_b()中)重排到a=1(set_a()中)之前; - 原因:屏障的核心是 “
a=1必须在屏障前执行,所有屏障后的内存操作(包括其他函数的b=2)必须在屏障后执行”,与函数边界无关。
生效范围:整个编译单元(.c 文件)的内存操作顺序,突破函数边界 --- 这一点存疑!!
编译器在优化时,会把整个编译单元(单个 .c 文件)作为整体重排指令,因此:
- 只要内存操作 A 在屏障前(无论 A 在哪个函数),内存操作 B 在屏障后(无论 B 在哪个函数),编译器就不能将 B 重排到 A 之前;
- 仅受 “编译单元” 限制(不同
.c文件编译为独立的目标文件,屏障无法跨编译单元生效),与函数无关。
常见误区¶
误区 1:认为能解决 CPU 层面的重排
❌ 错误:__asm__ volatile ("" ::: "memory") 仅禁止编译器重排,CPU 仍可能重排指令;
✅ 正确:多 CPU 架构下,需搭配硬件内存屏障(如 mfence/dmb)。
误区 2:滥用内存屏障
内存屏障会**牺牲编译器优化效率**,仅在并发场景(多线程 / 多进程共享变量)中使用;单线程无共享变量时,使用内存屏障会降低性能,无任何收益。
误区 3:替代 volatile
volatile 禁止编译器缓存单个变量到寄存器,而 memory 屏障禁止所有内存操作的重排 / 缓存,二者互补而非替代:
总结¶
__asm__ volatile ("" ::: "memory")是 GCC 编译期内存屏障,无实际指令执行,仅约束编译器行为;- 核心作用:禁止编译器重排内存读写指令、强制刷新寄存器缓存到主存;
- 仅作用于编译阶段,CPU 层面的重排需搭配硬件内存屏障;
- 典型场景:原子操作、自旋锁、并发共享变量,保证内存可见性和顺序性。
简单说:这个空汇编指令是给编译器的 “强制命令”——“别耍小聪明优化内存操作,必须按我写的顺序来,而且要老老实实读写主存,不能偷懒用寄存器里的旧值!”。
自旋锁¶
核心特性¶

实现加锁¶
nginx中如何实现自旋锁:
typedef int32_t ngx_atomic_int_t;
typedef uint32_t ngx_atomic_uint_t;
typedef volatile ngx_atomic_uint_t ngx_atomic_t;
// GCC编译器实现
#define ngx_atomic_cmp_set(lock, old, set) \
__sync_bool_compare_and_swap(lock, old, set)
#if ( __i386__ || __i386 || __amd64__ || __amd64 )
// x86_64体系使用pause指令
#define ngx_cpu_pause() __asm__ ("pause")
#else
// 无指令相当于空跑
#define ngx_cpu_pause()
#endif
// x86体系nginx手动实现
#define NGX_SMP_LOCK "lock;"
static ngx_inline ngx_atomic_uint_t
ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,
ngx_atomic_uint_t set)
{
u_char res;
__asm__ volatile (
NGX_SMP_LOCK
" cmpxchgl %3, %1; "
" sete %0; "
: "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory");
return res;
}
/**
* lock为需要比较的源值,value为预期值,spin为指数回退的最长尝试时间
*/
void ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
{
ngx_uint_t i, n;
for ( ;; ) {
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
if (ngx_ncpu > 1) {
for (n = 1; n < spin; n <<= 1) {
for (i = 0; i < n; i++) {
ngx_cpu_pause();
}
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
}
}
ngx_sched_yield();
}
}
上述代码设计要点:
- 本质上是在成功加锁之前,该函数会一直陷入循环
- 多核CPU下,为避免持续尝试CAS操作增加性能开销,会以spin为上限指数递增尝试;
-
大循环下,如果一轮CAS尝试失败,则yield主动让出调度:
-
基于系统调度sched_yield让出调度,让出调度放弃剩余时间片,加入就绪队列等待下次调度,状态仍保持TASK_RUNNING,无需中断唤醒;
-
基于sleep让出调度,状态切到TASK_INTERRUPTIBLE,需要定时器唤醒才能加入到就绪队列;
-
-
Nginx手搓的CAS实现,专为 x86/x86_64 架构优化,
lock前缀是 x86 原子操作的核心:强制cmpxchgl指令独占内存总线,禁止多 CPU 同时修改lock指向的内存,保证 SMP 架构下的原子性。
补充1:sched_yield() vs usleep(1) 核心差异(关键)
| 特性 | sched_yield() | usleep(1) |
|---|---|---|
| 进程状态 | 就绪态(TASK_RUNNING) | 可中断睡眠态(TASK_INTERRUPTIBLE) |
| CPU 占用 | 立即让出,无额外占用 | 休眠 1μs,期间不占用 CPU |
| 调度器行为 | 重新选择就绪进程调度 | 进程退出调度队列,1μs 后唤醒 |
| 开销 | 极低(仅上下文切换) | 较高(睡眠 / 唤醒 + 定时器) |
| 唤醒时机 | 立即就绪,可能被快速调度 | 至少 1μs 后(实际可能更长) |
| 适用场景 | 短时间自旋等待(如抢锁失败) | 无 sched_yield() 的兼容场景 |
补充2:GCC 内嵌汇编格式:__asm__ volatile ("汇编指令" : 输出约束 : 输入约束 : 破坏描述符);
| 约束段 | 内容 | 含义 |
|---|---|---|
| 输出约束 | "=a" (res) | "=a":输出到 eax 寄存器,且是 “只写”(=);res:C 语言变量,接收 sete 指令的结果(1/0);注: a 是 eax 寄存器的简写(x86 32 位)。 |
| 输入约束 | "m" (*lock), "a" (old), "r" (set) | "m" (*lock):*lock 作为内存操作数(m=memory),对应汇编中的 %1;"a" (old):old 加载到 eax 寄存器,对应汇编中的 %2;"r" (set):set 加载到任意通用寄存器(r=register),对应汇编中的 %3;注:输入约束的序号从 %1 开始(%0 是输出)。 |
| 破坏描述符 | "cc", "memory" | "cc":告诉编译器,指令修改了 CPU 状态寄存器(如 ZF 零标志位),禁止编译器依赖原有标志位优化;"memory":内存屏障,禁止编译器重排内存读写指令,保证操作的内存可见性。 |
实现解锁¶
自旋锁的 unlock(解锁)是自旋锁机制的关键一环,核心目标是**原子性释放锁,并保证解锁操作的内存可见性**,让其他自旋等待的线程能立即感知到锁已释放。
自旋锁的解锁必须满足 3 个核心条件(否则会导致并发安全问题):
- 原子性:解锁操作(将锁变量置 0)必须是原子的,避免多线程同时解锁导致锁状态混乱;
- 可见性:解锁后的锁状态必须立即对其他 CPU / 线程可见(禁止 CPU / 编译器缓存锁变量);
- 有序性:解锁前的临界区操作必须全部完成,禁止编译器 / CPU 将临界区操作重排到解锁之后。
解锁逻辑的关键细节(逐行拆解)
1、内存屏障(ngx_memory_barrier())
-
核心作用:
-
编译期:禁止编译器将 “临界区操作”(如修改队列)重排到解锁之后;
-
硬件层:
mfence强制 CPU 完成所有临界区的内存操作,刷新缓存到主存; -
保证:其他线程拿到锁后,能看到临界区的所有修改(如队列已更新)。
-
自旋锁解锁前加内存屏障,是为了**保证临界区操作的 “有序性” 和 “可见性”** —— 如果遗漏这一步,多 CPU / 多线程场景下会出现 “锁释放了,但临界区的修改还没同步出去” 的致命问题,导致其他线程拿到锁后读到脏数据,破坏并发安全。
- 解锁前的内存屏障核心解决**有序性**和**可见性**问题,是自旋锁并发安全的 “最后一道防线”;
- 无内存屏障时,编译器 / CPU 会重排指令、延迟刷新缓存,导致其他线程拿到锁后读到脏数据;
- 内存屏障强制:临界区操作必须在解锁前完成并同步到主存,其他线程才能正确感知;
- 单 CPU 需编译期屏障,多 CPU 需编译 + 硬件屏障。
// 全局变量
ngx_atomic_t lock = 0; // 自旋锁(0=解锁,1=锁定)
ngx_thread_task_t *queue = NULL; // 临界区:共享队列
// 线程A:加锁→修改队列→解锁(无内存屏障)
void thread_a() {
ngx_spinlock(&lock, 1, 0); // 加锁成功
// 临界区:修改队列(写操作)
ngx_thread_task_t *task = malloc(sizeof(ngx_thread_task_t));
task->next = queue;
queue = task;
// 错误:解锁前无内存屏障
lock = 0; // 解锁
}
// 线程B:自旋抢锁→读取队列
void thread_b() {
ngx_spinlock(&lock, 1, 0); // 抢到锁
// 读取队列:可能读到 NULL(脏数据)
if (queue == NULL) {
printf("队列空(错误)\n"); // 实际线程A已经修改了queue
}
lock = 0; // 解锁
}
2、原子置 0(*(lock) = 0)
- 原子性:对
ngx_atomic_t(32/64 位整型)的直接赋值是原子操作(CPU 单指令完成),无需 CAS; - 可见性:
ngx_atomic_t含volatile修饰,禁止编译器将赋值操作缓存到寄存器,必须直接写内存; - 状态切换:将锁变量从 “1(锁定)” 置为 “0(解锁)”,其他自旋的线程能立即读到 0,进而抢锁。
3、为什么不用 CAS 解锁?
加锁用 CAS(ngx_atomic_cmp_set),但解锁直接置 0,核心原因:
- 自旋锁的**持有者唯一**:只有抢到锁的线程会执行解锁,不存在 “多线程同时解锁” 的场景;
- CAS 解锁无意义:解锁时无需比较旧值,直接置 0 即可,CAS 会增加不必要的开销;
- 简化逻辑:保证解锁操作的极致性能
4、nginx加解锁案例
// step1: lock
ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);
// step2: 执行临界区代码
task = ngx_thread_pool_done.first;
ngx_thread_pool_done.first = NULL;
ngx_thread_pool_done.last = &ngx_thread_pool_done.first;
// step3:先执行内存屏障,保证内存可见性
ngx_memory_barrier();
#define ngx_unlock(lock) *(lock) = 0
ngx_unlock(&ngx_thread_pool_done_lock);
互斥量¶

TODO:底层原理是什么?如何阻塞?如何通知的?
读写锁¶
读写锁(也叫共享 - 排他锁)是一种**精细化的并发同步机制**,核心思想是 “区分读操作和写操作”:允许多个读线程同时持有锁(共享模式),但写线程必须独占锁(排他模式)。这种设计能极大提升 “读多写少” 场景(如配置读取、缓存查询、日志统计)的并发效率,是自旋锁 / 互斥锁的重要补充。
核心特性¶
1、核心规则
-
读锁(共享锁):
-
无写锁持有 → 多个读线程可同时加读锁;
-
有写锁持有 → 所有读线程阻塞,直到写锁释放。
-
-
写锁(排他锁):
-
无读 / 写锁持有 → 单个写线程可加写锁;
-
有读 / 写锁持有 → 写线程阻塞,直到所有读 / 写锁释放。
-
-
优先级:多数实现(如 POSIX)默认 “写优先”(写请求会插队,避免写线程饿死)。
| 特性 | 读写锁 | 自旋锁 / 互斥锁 |
|---|---|---|
| 访问模式 | 读共享(多个 Reader)、写排他(单个 Writer) | 全排他(无论读写,仅一个线程持有) |
| 并发效率 | 读多写少时极高(读线程无阻塞) | 所有场景都串行(并发效率低) |
| 阻塞策略 | 读锁:写锁持有则阻塞;写锁:读 / 写锁持有则阻塞 | 只要锁被持有,所有线程阻塞 / 自旋 |
| 适用场景 | 读多写少(如配置中心、缓存、统计报表) | 临界区极短、读写均衡 / 写多 |
底层实现(pthread)¶
1、核心结构(简化版)
typedef struct {
// 1. 读锁计数器:记录当前持有读锁的线程数
int read_count;
// 2. 写锁状态:0=未持有,1=持有
int write_lock;
// 3. 互斥锁:保护read_count/write_lock的原子性
pthread_mutex_t mutex;
// 4. 条件变量:阻塞/唤醒读/写线程
pthread_cond_t read_cond;
pthread_cond_t write_cond;
// 5. 写等待数:避免写线程饿死
int write_waiters;
} pthread_rwlock_t;
2、核心操作逻辑(伪代码)
(1)加读锁(pthread_rwlock_rdlock)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock) {
pthread_mutex_lock(&rwlock->mutex); // 保护内部状态
// 有写锁持有 或 有写线程等待(写优先)→ 阻塞读线程
while (rwlock->write_lock || rwlock->write_waiters > 0) {
pthread_cond_wait(&rwlock->read_cond, &rwlock->mutex);
}
rwlock->read_count++; // 读锁计数+1
pthread_mutex_unlock(&rwlock->mutex);
return 0;
}
(2)加写锁(pthread_rwlock_wrlock)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock) {
pthread_mutex_lock(&rwlock->mutex);
rwlock->write_waiters++; // 标记有写线程等待(写优先)
// 有读锁持有 或 有写锁持有 → 阻塞写线程
while (rwlock->read_count > 0 || rwlock->write_lock) {
pthread_cond_wait(&rwlock->write_cond, &rwlock->mutex);
}
rwlock->write_waiters--;
rwlock->write_lock = 1; // 独占写锁
pthread_mutex_unlock(&rwlock->mutex);
return 0;
}
(3)解锁(pthread_rwlock_unlock)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock) {
pthread_mutex_lock(&rwlock->mutex);
if (rwlock->write_lock) {
// 写锁解锁:重置写锁状态,优先唤醒写线程(写优先)
rwlock->write_lock = 0;
if (rwlock->write_waiters > 0) {
pthread_cond_signal(&rwlock->write_cond);
} else {
// 无写等待,唤醒所有读线程
pthread_cond_broadcast(&rwlock->read_cond);
}
} else {
// 读锁解锁:计数-1,若计数为0则唤醒写线程
rwlock->read_count--;
if (rwlock->read_count == 0 && rwlock->write_waiters > 0) {
pthread_cond_signal(&rwlock->write_cond);
}
}
pthread_mutex_unlock(&rwlock->mutex);
return 0;
}
读写锁的实战使用¶
读多写少场景:例如配置缓存
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define THREAD_NUM 10
// 全局配置(读多写少)
char *config = "default_config";
// 读写锁
pthread_rwlock_t rwlock;
// 读线程:频繁读取配置
void *read_config(void *arg) {
long tid = (long)arg;
while (1) {
// 加读锁(共享)
pthread_rwlock_rdlock(&rwlock);
printf("Reader %ld: config = %s\n", tid, config);
// 解锁
pthread_rwlock_unlock(&rwlock);
usleep(100000); // 模拟读耗时
}
return NULL;
}
// 写线程:偶尔更新配置
void *write_config(void *arg) {
long tid = (long)arg;
int count = 0;
while (1) {
// 加写锁(排他)
pthread_rwlock_wrlock(&rwlock);
// 临界区:修改配置(仅写线程可执行)
char new_config[32];
snprintf(new_config, sizeof(new_config), "config_%d", count++);
config = new_config;
printf("=== Writer %ld: update config to %s ===\n", tid, config);
// 解锁
pthread_rwlock_unlock(&rwlock);
sleep(1); // 模拟写耗时
}
return NULL;
}
int main() {
pthread_t threads[THREAD_NUM];
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 创建8个读线程,2个写线程(读多写少)
for (long i = 0; i < 8; i++) {
pthread_create(&threads[i], NULL, read_config, (void *)i);
}
for (long i = 8; i < 10; i++) {
pthread_create(&threads[i], NULL, write_config, (void *)i);
}
// 等待线程结束(实际不会结束)
for (int i = 0; i < THREAD_NUM; i++) {
pthread_join(threads[i], NULL);
}
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
编译运行
输出效果(核心特征)
Reader 0: config = default_config
Reader 1: config = default_config
...(8个读线程同时输出,无阻塞)
=== Writer 8: update config to config_0 ===
Reader 0: config = config_0
Reader 1: config = config_0
...(写线程执行时,所有读线程阻塞,写完成后读线程继续)
读写锁的关键优化点¶
1、写优先 vs 读优先
- 写优先(默认):写请求会插队,避免写线程 “饿死”(一直抢不到锁);
- 读优先:读请求优先,并发更高,但可能导致写线程饿死;
- 可通过读写锁属性(
pthread_rwlockattr_t)配置:
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
// 设置写优先(POSIX默认,部分系统需显式配置)
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NP);
pthread_rwlock_init(&rwlock, &attr);
2、自旋读写锁(Nginx / 内核级实现)
POSIX 读写锁是 “阻塞式”(依赖条件变量),而 Nginx/Linux 内核实现了**自旋读写锁**(纯用户态,无内核切换),核心优化:
- 读锁:原子递增计数器(无自旋,直接成功);
- 写锁:自旋等待读计数器为 0,再原子置位写锁;
- 适用场景:临界区极短(<100 纳秒)的读多写少场景。
Nginx 自旋读写锁核心逻辑(简化)
typedef struct {
ngx_atomic_t readers; // 读计数器(负数表示写锁持有)
} ngx_rwlock_t;
// 加读锁:readers++(原子操作)
#define ngx_rwlock_rdlock(lock) \
do { \
ngx_atomic_fetch_add(&lock->readers, 1); \
// 若readers为负(写锁持有),自旋等待 \
while (ngx_atomic_cmp_set(&lock->readers, -1, -1)) { \
ngx_cpu_pause(); \
} \
} while (0)
// 加写锁:自旋等待readers=0,再置为-1(排他)
#define ngx_rwlock_wrlock(lock) \
do { \
while (!ngx_atomic_cmp_set(&lock->readers, 0, -1)) { \
ngx_cpu_pause(); \
} \
} while (0)
// 解锁:读锁则readers--,写锁则置为0
#define ngx_rwlock_unlock(lock) \
do { \
if (ngx_atomic_fetch_add(&lock->readers, -1) == -1) { \
ngx_atomic_set(&lock->readers, 0); \
} \
} while (0)
读写锁的常见坑点¶
1、读锁不保证 “最新性”(需配合内存屏障)
多个读线程共享锁时,若有写线程更新数据,读线程需依赖内存屏障才能看到最新值:
// 写线程:更新后加内存屏障
pthread_rwlock_wrlock(&rwlock);
config = new_config;
__asm__ volatile ("mfence" ::: "memory"); // 硬件屏障
pthread_rwlock_unlock(&rwlock);
// 读线程:读取前加内存屏障
pthread_rwlock_rdlock(&rwlock);
__asm__ volatile ("lfence" ::: "memory"); // 读屏障
printf("config = %s\n", config);
pthread_rwlock_unlock(&rwlock);
2、避免 “读锁升级为写锁”
读线程持有读锁时,尝试加写锁会导致**死锁**(自己阻塞自己):
// 错误:读锁未释放,加写锁→死锁
pthread_rwlock_rdlock(&rwlock);
pthread_rwlock_wrlock(&rwlock); // 阻塞,且其他读线程也持有锁→永久死锁
// 正确:先释放读锁,再加写锁
pthread_rwlock_rdlock(&rwlock);
// 读取数据
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_wrlock(&rwlock);
// 修改数据
3、读写锁并非 “银弹”
- 写操作频繁时,读写锁效率不如互斥锁(写锁排他,且有额外的计数器开销);
- 临界区极短时,自旋锁 / 互斥锁的开销更低(读写锁的计数器操作有额外开销)。
总结¶
- 读写锁核心是 “读共享、写排他”,极大提升读多写少场景的并发效率;
- 底层实现分两类:POSIX 阻塞式(依赖互斥锁 + 条件变量)、内核 / NGINX 自旋式(纯用户态);
- 核心规则:写锁排他,读锁共享;默认写优先,避免写线程饿死;
- 常见坑:读锁升级写锁会死锁、读线程需内存屏障保证数据最新性。
简单说:读写锁就像 “图书馆规则”—— 多人可同时看书(读共享),但有人要修改书籍内容(写排他)时,必须等所有人看完,且修改时不允许任何人看书;修改完成后,又能多人同时看,既保证了并发,又保证了修改的独占性。
文件锁¶
文件锁是**操作系统提供的、基于文件描述符的同步机制**,核心作用是保证多个进程(甚至跨机器)对同一文件的读写操作互斥 / 共享,避免文件数据错乱。它和自旋锁、读写锁的核心区别是:文件锁作用于**文件**而非内存变量,支持跨进程、跨会话,甚至通过网络文件系统(NFS)跨机器同步。
核心特性¶
| 特性 | 文件锁(flock/fcntl) | 内存锁(自旋锁 / 读写锁) |
|---|---|---|
| 作用范围 | 跨进程、跨机器(NFS) | 仅进程内多线程 |
| 同步对象 | 文件(文件描述符 / 路径) | 内存变量 |
| 阻塞策略 | 阻塞 / 非阻塞(内核态等待) | 自旋(用户态)/ 阻塞(内核态) |
| 持久化 | 锁随文件描述符关闭释放,支持强制解锁 | 锁随进程退出释放 |
| 适用场景 | 多进程共享文件(如日志、配置、数据库) | 进程内多线程共享内存 |
文件锁主要分两类,对应不同的系统调用:
- 建议性锁(Advisory Lock):仅 “约定” 遵守锁规则,不强制(进程可无视锁直接读写);
- 强制性锁(Mandatory Lock):内核强制生效,无视锁的进程读写会被阻塞 / 报错(需文件系统开启);
实际开发中 99% 用**建议性锁**(强制性锁性能差、兼容性低)。
两种核心实现¶
简易文件锁:flock(整文件锁)¶
flock 是简化版文件锁,只能锁定整个文件(无法锁定文件部分),接口简单,适合大部分场景。
1、核心接口
#include <sys/file.h>
// 加锁/解锁
// fd:文件描述符(必须以读写/只读/只写方式打开)
// operation:
// LOCK_SH:共享锁(读锁,多个进程可同时持有)
// LOCK_EX:排他锁(写锁,仅一个进程持有)
// LOCK_UN:解锁
// LOCK_NB:非阻塞(加锁失败立即返回,不阻塞)
int flock(int fd, int operation);
实战示例(多进程写日志,排他锁)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <fcntl.h>
#include <pthread.h>
// 写日志函数(加排他锁保证原子写)
void write_log(const char *msg) {
// 打开文件(O_APPEND保证写在末尾,O_CREAT创建文件)
int fd = open("app.log", O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) {
perror("open failed");
return;
}
// 加排他锁(阻塞式,直到拿到锁)
if (flock(fd, LOCK_EX) < 0) {
perror("flock failed");
close(fd);
return;
}
// 临界区:写日志(原子操作)
write(fd, msg, strlen(msg));
write(fd, "\n", 1);
// 解锁
flock(fd, LOCK_UN);
// 关闭文件(即使忘解锁,关闭fd也会自动释放锁)
close(fd);
}
// 子进程函数:循环写日志
void *child_process(void *arg) {
long pid = (long)arg;
char msg[64];
for (int i = 0; i < 5; i++) {
snprintf(msg, sizeof(msg), "Process %ld: log %d", pid, i);
write_log(msg);
usleep(100000); // 模拟耗时
}
return NULL;
}
int main() {
// 创建3个进程写日志
for (long i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) { // 子进程
child_process((void *)i);
exit(0);
} else if (pid < 0) {
perror("fork failed");
}
}
// 等待所有子进程结束
for (int i = 0; i < 3; i++) {
wait(NULL);
}
printf("All processes done. Check app.log\n");
return 0;
}
编译运行
输出效果(核心:日志行完整,无重叠)
高级文件锁:fcntl(细粒度锁)¶
fcntl 是更强大的文件锁接口,支持:
- 锁定文件的**部分区域**(如第 100-200 字节);
- 跨进程传递锁;
- 自定义锁的行为(如信号通知);
- 兼容 POSIX 标准,跨平台性更好。
核心接口
#include <fcntl.h>
// 锁结构:描述要锁定的文件区域
struct flock {
short l_type; // 锁类型:F_RDLCK(读锁)、F_WRLCK(写锁)、F_UNLCK(解锁)
short l_whence; // 偏移起始位置:SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)
off_t l_start; // 起始偏移(字节)
off_t l_len; // 锁定长度(字节,0表示到文件末尾)
pid_t l_pid; // 持有锁的进程ID(仅用于查询)
};
// 加锁/解锁/查询锁
// fd:文件描述符
// cmd:
// F_SETLK:设置锁(非阻塞,失败返回-1)
// F_SETLKW:设置锁(阻塞,直到拿到锁)
// F_GETLK:查询锁(填充flock结构,返回持有锁的进程)
int fcntl(int fd, int cmd, struct flock *lock);
实战示例(锁定文件部分区域)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// 锁定文件的指定区域
int lock_file_region(int fd, off_t start, off_t len, int is_write) {
struct flock lock = {0};
lock.l_whence = SEEK_SET;
lock.l_start = start;
lock.l_len = len;
// 读锁/写锁
lock.l_type = is_write ? F_WRLCK : F_RDLCK;
// 阻塞式加锁
return fcntl(fd, F_SETLKW, &lock);
}
// 解锁文件区域
int unlock_file_region(int fd, off_t start, off_t len) {
struct flock lock = {0};
lock.l_whence = SEEK_SET;
lock.l_start = start;
lock.l_len = len;
lock.l_type = F_UNLCK;
return fcntl(fd, F_SETLK, &lock);
}
int main() {
// 打开文件(读写模式)
int fd = open("data.bin", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open failed");
return -1;
}
// 锁定文件第0-100字节(写锁)
if (lock_file_region(fd, 0, 100, 1) < 0) {
perror("lock failed");
close(fd);
return -1;
}
// 临界区:修改第0-100字节
char data[100] = "Hello File Lock!";
lseek(fd, 0, SEEK_SET);
write(fd, data, strlen(data));
// 解锁
unlock_file_region(fd, 0, 100);
close(fd);
return 0;
}
关键细节¶
1、锁的释放规则(必须牢记)
flock:锁与**文件描述符**绑定,关闭 fd 会自动释放锁(即使忘调用LOCK_UN);fcntl:锁与**进程 + 文件**绑定,进程退出会自动释放锁;- 注意:
dup/fork复制的 fd,flock会认为是同一个锁(共享),fcntl则是独立锁。
2、非阻塞加锁(避免死锁)
// flock非阻塞加锁
if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
if (errno == EWOULDBLOCK) {
printf("文件已被锁定,非阻塞加锁失败\n");
return -1;
}
}
// fcntl非阻塞加锁
struct flock lock = {0};
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLK, &lock) < 0) {
if (errno == EAGAIN || errno == EACCES) {
printf("文件区域已被锁定\n");
return -1;
}
}
3、建议性锁 vs 强制性锁
- 建议性锁(默认):所有进程必须 “主动加锁 / 检查锁” 才生效,若有进程直接读写文件,锁会失效;
-
强制性锁:需满足两个条件:
-
文件挂载时开启
-o mand(如mount -o mand /dev/sda1 /mnt); -
文件设置
setgid位,且组执行位为 0(chmod g+s,g-x file);
-
强制性锁性能差,仅在必须强制约束所有进程时使用。
4、跨机器文件锁(NFS)
flock:不支持 NFS(仅本地文件系统有效);fcntl:支持 NFS v3+(需服务端开启锁服务);- 跨机器场景优先用
fcntl,或借助分布式锁(如 Redis/ZooKeeper)。
文件锁的常见坑点¶
1、只写 / 只读打开文件的锁限制
- 加**读锁(LOCK_SH/F_RDLCK)**:文件必须以
O_RDONLY/O_RDWR打开; - 加**写锁(LOCK_EX/F_WRLCK)**:文件必须以
O_WRONLY/O_RDWR打开;
错误示例:
2、多个 fd 指向同一文件的锁冲突
fork/dup复制的 fd,flock会认为是 “同一个锁”(不会冲突),但fcntl会认为是 “不同锁”(可能死锁):
int fd1 = open("file.txt", O_RDWR);
int fd2 = dup(fd1);
flock(fd1, LOCK_EX);
flock(fd2, LOCK_EX); // 成功(同一锁)
struct flock lock = {.l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 0};
fcntl(fd1, F_SETLKW, &lock);
fcntl(fd2, F_SETLKW, &lock); // 死锁(不同锁,互相等待)
3、忘解锁导致锁残留
虽然关闭 fd 会自动解锁,但长期打开 fd 且忘解锁,会导致其他进程一直阻塞:
int fd = open("file.txt", O_RDWR);
flock(fd, LOCK_EX);
// 忘记解锁,且fd未关闭 → 锁一直持有
// 解决:用RAII(C++)或goto保证解锁/关闭fd
总结¶
- 文件锁是跨进程的同步机制,分
flock(整文件,简单)和fcntl(细粒度,强大); - 核心类型:共享锁(读)、排他锁(写),默认是建议性锁;
- 锁与 fd / 进程绑定,关闭 fd / 进程退出会自动释放锁;
- 适用场景:多进程共享文件(日志、配置、数据库文件),跨进程同步;
- 跨机器优先用
fcntl(支持 NFS),或分布式锁。
简单说:文件锁就像 “文件的门禁卡”—— 共享锁是 “多人可同时进门看书”,排他锁是 “仅一人进门修改”;flock是简易门禁(管整个房子),fcntl是高级门禁(管单个房间),核心都是保证文件操作的原子性和一致性。
条件变量¶
互斥量解决的是多线程并发读写的安全问题,条件变量解决的是多线程下协作的通知和等待问题。两者是配合关系,解决的不同问题。条件变量典型应用在生产消费问题上。


RCU¶
各类锁场景对比¶
在多进程 / 多线程同步场景中,锁的设计围绕 “资源独占、并发优化、场景适配” 三大目标,核心可分为 基础独占锁、并发优化锁、跨进程锁、特殊场景锁 四大类,下面按 “类型定义 + 核心特性 + 适用场景” 拆解,附选型优先级,新手也能快速匹配场景。
一、基础独占锁(核心基石,必掌握)
这类锁的核心是 “保证资源独占访问”,是所有同步锁的基础,适用于绝大多数基础场景。
| 锁类型 | 核心定义 | 核心特性 | 适用场景 |
|---|---|---|---|
| 互斥锁(Mutex) | 二元锁(0/1),同一时间仅 1 个线程 / 进程持有,保证资源独占 | ✅ 有所有权(仅加锁者可解锁) ✅ 获取失败则阻塞(让出 CPU) ❌ 不支持并行读 | 通用独占场景:文件读写、全局变量修改、临界区代码执行(读写均衡 / 写多读少) |
| 递归互斥锁(RLock) | 互斥锁的扩展,允许同一线程多次加锁(需对应次数解锁) | ✅ 解决 “递归调用加锁” 问题 ✅ 其他特性同 Mutex | 递归函数、嵌套加锁场景(如函数 A 调用函数 B,两者都需加锁) |
| 自旋锁(Spin Lock) | 忙等版互斥锁,获取失败时不阻塞,循环检测锁状态(不放弃 CPU) | ✅ 无上下文切换开销(性能极高) ❌ 自旋耗 CPU ✅ 多核场景优势明显 | 多核 CPU + 临界区极短(<10 微秒):内核态代码、高频内存操作、低延迟交易系统 |
关键提醒
- 自旋锁 仅适用于多核 + 短临界区,单核 / 长临界区使用会导致 CPU 100% 占用;
- 递归互斥锁需严格保证 “加锁次数 = 解锁次数”,否则会死锁。
二、并发优化锁(读多写少 / 多资源场景)
这类锁是基础锁的 “场景化优化”,解决基础锁 “全量串行” 导致的性能浪费问题。
| 锁类型 | 核心定义 | 核心特性 | 适用场景 |
|---|---|---|---|
| 读写锁(Read-Write Lock) | 分读锁(共享锁)、写锁(排他锁):多读并行、写写 / 读写互斥 | ✅ 读多写少场景性能远超 Mutex ✅ 可配置优先级(读优先 / 写优先) ❌ 实现复杂 | 读占比≥80% 的场景:配置中心、缓存查询、日志查看、商品库存查询 |
| 信号量(Semaphore) | 计数锁(0~N),控制同时访问资源的线程 / 进程数(本质是 “多资源版 Mutex”) | ✅ 无所有权(任意线程可释放) ✅ 支持同步 + 互斥 ❌ 易误释放导致异常 | 有限资源池控制:连接池(最多 10 个连接)、线程池、限流(最多 5 个并发请求) |
| 条件变量(Condition) | 基于 Mutex 的扩展,实现 “等待 - 唤醒” 机制(非纯锁,配合锁使用) | ✅ 精准唤醒指定线程 / 所有线程 ✅ 解决 “轮询等待” 问题 | 生产者 - 消费者模型、任务排队执行、线程间同步通信 |
关键提醒
- 信号量不是 “纯锁”,但常被归为同步锁范畴,不要用信号量实现简单互斥(无所有权,易出错);
- 读写锁避免 “读锁升级为写锁”(先加读锁再加写锁会导致死锁)。
三、跨进程锁(多进程场景专属)
多线程锁(如 Python threading.Lock)仅在进程内有效,跨进程需用内核级 / 文件级锁。
| 锁类型 | 核心定义 | 核心特性 | 适用场景 |
|---|---|---|---|
| 文件锁(fcntl/portalocker) | 基于文件的跨进程锁,通过内核维护文件锁状态 | ✅ 跨进程 / 跨机器(网络文件) ✅ 兼容所有系统 ❌ 性能略低(涉及文件 IO) | 多进程共享文件、跨进程资源独占(如多进程写同一个日志文件) |
| 系统 V 共享内存锁(shmctl) | 基于 System V 共享内存的跨进程锁 | ✅ 纯内存操作(性能高) ❌ 接口老旧、移植性差 | 老系统跨进程同步、高性能跨进程内存共享 |
| POSIX 互斥锁(PThread Mutex) | 跨进程的 POSIX 标准锁(需映射到共享内存) | ✅ 性能高、接口统一 ✅ 支持进程 / 线程级锁定 | 现代 Linux/Unix 多进程同步、跨进程临界区保护 |
| 分布式锁(Redis/ZooKeeper) | 跨机器 / 跨集群的分布式锁(非系统级锁,属于应用层锁) | ✅ 跨网络、跨集群 ✅ 支持超时、重入 ❌ 依赖中间件 | 分布式系统:分布式任务调度、跨机器资源独占、秒杀系统库存保护 |
关键提醒
- Python 多进程推荐用
multiprocessing.Lock(封装了 POSIX 锁),无需手动实现文件锁; - 分布式锁优先选 Redis(高性能)或 ZooKeeper(高可靠),避免手动实现(易出超时 / 死锁问题)。
四、特殊场景锁(解决特定问题)
这类锁针对 “死锁、公平性、优先级” 等特殊需求设计,属于进阶用法。
| 锁类型 | 核心定义 | 核心特性 | 适用场景 |
|---|---|---|---|
| 公平锁(Fair Lock) | 按 “先到先得” 顺序分配锁,避免线程饥饿 | ✅ 解决 “锁抢占不均” 问题 ❌ 性能略低(需维护等待队列) | 对公平性要求高的场景:任务调度、资源分配 |
| 重入锁(Reentrant Lock) | 同 “递归互斥锁(RLock)”,允许同一线程多次加锁 | ✅ 避免递归调用死锁 ✅ 有所有权保护 | 嵌套函数加锁、回调函数加锁 |
| 屏障锁(Barrier) | 等待所有线程到达指定点后,再一起执行 | ✅ 实现 “同步点” 机制 ✅ 支持超时、重置 | 多线程协同任务:数据分片计算(所有分片计算完成后汇总) |
| 死锁检测锁(Deadlock Detection Lock) | 内置死锁检测逻辑,触发死锁时报警 / 解锁 | ✅ 调试 / 监控场景实用 ❌ 性能开销大 | 开发 / 测试阶段、复杂锁嵌套场景(定位死锁问题) |
五、锁选型优先级(新手直接套用)
(1)多线程场景
- 通用独占 → Mutex(threading.Lock);
- 读多写少 → 读写锁(第三方库如
rwlock); - 递归 / 嵌套加锁 → RLock(threading.RLock);
- 多核 + 短临界区 → 自旋锁(Python 需模拟,C/C++ 原生支持);
- 线程同步通信 → Condition(threading.Condition)。
(2)多进程场景
- 简单跨进程独占 → multiprocessing.Lock;
- 多进程共享文件 → 文件锁(portalocker);
- 高性能跨进程内存共享 → POSIX 互斥锁 / System V 锁;
- 跨机器 / 集群 → Redis/ZooKeeper 分布式锁。
六、避坑总原则
- 优先用 “最简单的锁”:能用水 Mutex 解决的,不用读写锁 / 信号量(减少复杂度);
- 跨进程锁优先选 “内核级”(如
multiprocessing.Lock),而非手动实现文件锁; - 自旋锁仅用于 “多核 + 极短临界区”,否则不如 Mutex 高效;
- 分布式锁必须加 “超时机制”,避免网络异常导致死锁。
七、锁总结
- 核心分类:基础独占锁(Mutex / 自旋锁)→ 并发优化锁(读写锁 / 信号量)→ 跨进程锁(文件锁 / 分布式锁)→ 特殊场景锁(公平锁 / 屏障锁);
- 选型核心:先看 “线程 / 进程”→ 再看 “读写比例 / 资源数量”→ 最后看 “特殊需求(公平性 / 递归)”;
- 关键提醒:锁的本质是 “牺牲并发换安全”,避免过度加锁(如无竞争的代码段加锁)。