【基础知识】互斥锁、读写锁、自旋锁的区别

内容分享2天前发布
0 0 0

定义、工作原理、适用场景和性能开销四个维度来剖析这三种锁的区别


核心结论

这三种锁的核心区别在于它们应对“锁已被占用”情况时的行为策略不同,而这直接决定了它们的性能和适用场景。

锁类型 核心策略 适用场景
互斥锁 (Mutex) 等不到,就睡 通用的独占访问,临界区执行时间较长
自旋锁 (SpinLock) 等不到,就循环问 临界区执行时间非常短,且不希望线程切换开销
读写锁 (RWLock) 读共享,写独占 读多写少的场景,大幅提升读取性能

为了更直观地理解它们的行为差异,可以参考下面的决策流程图,它展示了根据你的需求选择最合适锁类型的路径:


flowchart TD
    A[开始选择锁类型] --> B{需要允许多个读者<br>同时访问吗?};

    B -- 否 --> C{临界区代码<br>执行时间非常短吗?};
    B -- 是 --> E[选择读写锁 RWLock];

    C -- 否 --> D[选择互斥锁 Mutex<br>原因:让出CPU,避免忙等];
    C -- 是 --> F[选择自旋锁 SpinLock<br>原因:避免线程切换开销];

    E --> G[特点:读共享,写独占<br>适用:读多写少的场景(如缓存)];
    D --> H[特点:等不到就睡眠<br>适用:通用独占访问];
    F --> I[特点:等不到就循环询问<br>适用:内核态、短临界区];

下面我们对每种锁进行详细解读。


1. 互斥锁 (Mutex)

工作方式

当一个线程试图获取一个已被占用的互斥锁时,该线程会被挂起(Sleep),并加入到该锁的等待队列中。当锁被释放时,操作系统会唤醒等待队列中的一个线程,使其获得锁并继续执行。

底层机制:依赖于操作系统的调度器进行线程切换(用户态 <-> 内核态的切换)。

开销

两次上下文切换开销:获取锁失败时,线程从运行态切换到睡眠态;锁被释放时,线程从睡眠态切换到就绪态。开销较大,通常在微秒级。

优点:在等待期间不消耗CPU时间片,CPU可以去做其他事情。

缺点:线程切换带来的延迟较高,对于极短的临界区,切换开销可能远大于执行开销。

比喻你在超市结账,但所有柜台都排着队。你选择直接找个地方坐下休息(被挂起),等收银员叫你的号(唤醒)再去结账。


2. 自旋锁 (SpinLock)

工作方式

当一个线程试图获取一个已被占用的自旋锁时,它会在一个循环中不断地尝试获取锁(忙等待,Busy-Waiting),直到成功为止。线程始终处于运行状态,不会让出CPU。

底层机制:通常基于CPU提供的原子操作(如CAS, Compare-And-Swap)实现。

开销

在等待期间,持续消耗CPU时间片。如果锁被占用的时间很短,自旋锁的开销(几次循环)远小于互斥锁的线程切换开销。

优点:避免了线程切换的开销,获取锁的速度非常快(如果等待时间短)。

缺点:如果锁被长时间占用,会白白浪费CPU资源,降低系统整体吞吐量。

适用场景

临界区代码执行时间极短(通常小于两次线程上下文切换的时间)。在多核处理器上(在单核处理器上自旋通常没有意义,因为持有锁的线程无法运行)。常用于操作系统内核中,因为内核中断处理等场景不能进行线程调度。

比喻你在超市结账,看到一个柜台暂时关闭。你不停地问他“好了吗?”(循环询问),而不是离开去做别的事。


3. 读写锁 (Read-Write Lock)

工作方式

它区分了“读锁”和“写锁”。读锁(共享锁):允许多个线程同时持有读锁,以进行并发读取。写锁(排他锁):只允许一个线程持有写锁,且此时不能有任何读锁或其他写锁。写锁是排他的。读写互斥:当一个线程持有写锁时,其他所有请求读锁和写锁的线程都必须等待。写读互斥:当有线程持有读锁时,请求写锁的线程必须等待,直到所有读锁释放。

底层机制:内部通常使用互斥锁和条件变量来实现上述复杂的同步规则。

开销:本身的管理开销比互斥锁稍大,但它通过允许多个读操作并发执行,极大地提高了读多写少场景下的性能。

优点:极大地提高了读操作的并发性

缺点

实现复杂,开销略大。如果写操作很频繁,读写锁的性能可能不如直接使用互斥锁,因为管理锁状态的开销会增大。容易导致“写者饥饿”(Readers-Writers Problem),即如果一直有新的读请求到来,写者可能永远无法获取锁。

比喻一个公司的公告板。

读操作:很多员工可以同时看(共享读)公告板上的内容。写操作:但当一个经理要更新(独占写)公告时,必须等所有员工看完离开后,他才能张贴新公告,并且在张贴过程中,所有员工都不能看。


总结与对比表格

特性 互斥锁 (Mutex) 自旋锁 (SpinLock) 读写锁 (RWLock)
锁被占用时的行为 睡眠等待 忙等待(循环) 读:可共享;写:睡眠等待
开销来源 线程上下文切换(巨大) 循环消耗CPU(小或大) 管理开销(中等)
优点 等待时不消耗CPU 无切换延迟,速度快 读操作并发性高
缺点 上下文切换开销大 浪费CPU,可能饥饿 实现复杂,可能写者饥饿
适用场景 通用场景,临界区执行时间较长 临界区极短,多核CPU,内核开发 读多写少(缓存、配置)

如何选择?

默认选择互斥锁:在大部分应用层代码中,临界区代码执行时间不确定或不是很短,优先使用互斥锁。它的行为最稳妥。考虑自旋锁:如果你能确定临界区的代码执行时间非常短(例如,只是几条原子指令,修改一个指针或计数器),并且在多核CPU上,可以考虑使用自旋锁来榨取极致性能。考虑读写锁:当你的数据结构存在明显的读多写少(例如90%的读,10%的写)的访问模式,并且读操作耗时较长时,使用读写锁可以带来巨大的性能提升。

重要提示:在用户态应用程序开发中,应优先使用标准库(如
std::mutex
)或系统提供的同步原语,它们通常已经过高度优化,并在不同场景下可能混合使用了自旋和休眠策略(例如,Linux下的
pthread_mutex
可能会先自旋一小段时间再休眠),以达到最佳性能。不要轻易自己实现自旋锁。

© 版权声明

相关文章

暂无评论

none
暂无评论...