浅谈一下多线程编程中的锁机制
锁机制是多线程编程中控制共享资源访问的核心工具。文章从物理锁的类比出发,阐述了编程锁的本质是排他性访问控制,并通过详细分类表对比了各类锁的特性。C++标准库提供了多种互斥锁(基础互斥、可重入锁、超时锁)、读写锁以及RAII管理工具,适用于不同并发场景。锁的选择需权衡互斥性、可重入性、公平性和性能开销等因素,以平衡数据安全性与程序效率。
·
1 前言
什么是锁,为什么需要锁?
我们知道,线程是操作系统调度的最小单位,多个线程可以并发的执行,并在过程中读写一些线程间的共享数据,读写数据同时发生,可能会导致数据读取异常(读到不完整的数据,或者读到脏数据等等),或者是说某一时间,我只希望单个线程执行一段特定逻辑,这个时候,我们需要一种机制使多个线程串行化的执行读写数据或者是执行其他任务,于是锁,信号量,条件变量等用于管理操作系统中多线程或者多进程并发访问的工具诞生了,这一类工具,我们也称之于 同步原语
(synchronization primitives);
那么锁是什么?
- 物理锁的本质: 在现实世界中,锁(比如门锁、保险柜锁)的核心作用是控制对某个受限资源(如房间、物品)的访问权。拥有钥匙(权限)的人可以“解锁”并使用资源,用完后再“上锁”,阻止他人同时进入或触碰。
- 编程锁的本质: 在多线程环境中,“锁”机制的核心作用同样是控制对某个受限资源(如共享变量、数据结构)的并发访问权。拥有锁的线程可以“获得锁”(加锁)并进入“临界区”(操作资源),操作完后“释放锁”(解锁),阻止其他线程同时进入该临界区进行操作。
- 关键映射: “获得锁 = 开门进入房间”,“释放锁 = 关门上锁”,“临界区 = 房间里的东西”,“其他线程 = 想进入房间的其他人”。这个访问控制、排他性的概念与物理锁完全一致。
简单来说,上锁就是为了通过一种排他性的“持有-释放”机制来控制对共享资源的访问权,就像物理锁控制对物理空间的访问一样。
📊 锁分类总结表
分类维度 | 类别 | 核心特征 | 优点 | 缺点 |
---|---|---|---|---|
互斥性 | 🔒 互斥锁 | 一次仅允许一个线程持有锁 (绝对排他) | 简单可靠,保证数据完整性和操作原子性 | 并发度低,可能成为瓶颈 |
📚 非互斥锁 | 允许多个线程以特定方式持有锁 | 提高特定场景(尤其读多写少)的并发度 | 比互斥锁复杂,读写锁需区分读写模式 | |
可重入性 | 🔄 可重入锁 | 持有锁的线程可多次成功获取同一锁 (避免自死锁) | 支持在同步代码中递归调用需要同一锁的函数/方法 | 实现稍复杂,内部需维护持有计数和线程标识 |
🔐 非可重入锁 | 持有锁的线程再次获取同一锁会死锁/阻塞 | 可能更简单或轻量 | 极易因递归调用导致死锁 | |
公平性策略 | 🎯 公平锁 | 按线程请求锁的顺序分配锁 | 理论上避免饥饿(所有线程最终得执行) | 性能通常较低 (维护队列, 顺序唤醒, 上下文切换多) |
🚀 非公平锁 | 不严格按请求顺序分配锁, 新到请求或等待线程可能竞争抢到锁 ("插队") | 通常性能更高 (尤其高竞争时) | 可能导致线程饥饿(某个线程长时间抢不到锁) | |
阻塞行为 | 😴 阻塞锁 | 获取失败时线程挂起(阻塞), 不消耗CPU, 等待唤醒 | 适合持有锁时间较长的操作 | 上下文切换开销大, 可能无限期等待 |
⏱️ 自旋锁 | 获取失败时忙等待(CPU空转), 不断检查锁状态 | 锁持有极短时获取极快 (省上下文切换) | 持锁时间长时极其浪费CPU, 单核无意义 | |
✨ 乐观锁 | 无锁机制: 使用版本号/CAS, 检查冲突才重试 | 无阻塞, 高并发(无争用/低争用时) | 冲突高时重试开销大, 需要应用程序处理冲突 | |
并发控制范式 | 😢悲观锁 |
“先加锁,后操作” C++ 中绝大多数锁机制都属于此范式! |
简单直接,保证强一致性。 | 加锁开销、死锁风险、阻塞可能。 |
✨ 乐观锁 | “先操作,后验证” 假设冲突罕见,先无锁进行读写操作,提交前验证数据是否被修改(如版本号/CAS)。若冲突则重试/处理。 |
无锁或轻锁,高并发潜力。 | 冲突处理逻辑复杂,重试开销(高冲突时),弱一致性可能。 | |
作用域层级 | 🌐分布式锁 | 协调跨进程/跨机器对共享资源(如文件、数据库记录、服务)的访问。 | 解决网络分区、节点故障下的全局互斥。 | 核心挑战: 时钟漂移、脑裂、锁释放(租约/心跳)。 |
📁数据库锁 | DBMS 内部实现的锁机制,粒度更细(表锁、页锁、行锁、意向锁) | ACID 事务保障的核心。 隔离级别(RU, RC, RR, Serializable)直接影响锁策略。 意向锁提升并发(表级 IS/IX)。 |
隔离级别(RU, RC, RR, Serializable)直接影响锁策略。 |
🔒 C++ 锁机制分类详解
分类维度 | 类型/工具 | 核心特性 | 适用场景 |
---|---|---|---|
基础互斥类型 | std::mutex |
基础互斥锁,独占访问 | 简单临界区保护 |
std::recursive_mutex |
可重入互斥锁,允许同一线程多次加锁 | 递归函数/类内互斥调用 | |
读写锁 | std::shared_mutex |
读写分离:多线程共享读,单线程独占写 | 读多写少场景 (配置读取/数据缓存) |
std::shared_timed_mutex |
支持超时功能的读写锁 | 读多写少+需要加锁超时控制 | |
超时锁变体 | std::timed_mutex |
支持超时的基础互斥锁 | 避免死锁阻塞 (实时系统/网络超时) |
std::recursive_timed_mutex |
支持超时的可重入锁 | 递归调用+超时需求 | |
RAII 管理工具 | std::lock_guard |
作用域锁:构造时自动加锁,析构自动解锁 | 简单作用域锁定 (推荐默认使用) |
std::unique_lock |
灵活锁:支持延迟加锁、转移所有权、条件变量配合、手动解锁 | 复杂锁管理 (条件变量/锁策略/多锁协同) | |
std::shared_lock |
共享锁守卫:配合 shared_mutex 实现自动读锁管理 |
读写锁的读模式保护 | |
高级同步原语 | std::atomic |
无锁原子操作:硬件级指令保证操作原子性 (CAS/TAS) | 简单标志位/计数器 高性能无锁场景 |
std::atomic_flag |
轻量原子标志:唯一保证无锁的原子类型 | 自旋锁/低开销互斥实现基础 | |
std::counting_semaphore |
计数信号量:控制 N 个资源的并发访问 | 连接池/缓冲区槽位管理 | |
线程局部存储 | thread_local |
线程私有变量:每个线程拥有独立副本 | 线程独立状态 (ID/缓存/上下文) 彻底避免同步 |
多锁协同工具 | std::lock() |
多锁死锁回避:原子化锁定多个互斥量 | 同时获取多个锁 避免死锁链风险 |
std::try_lock() |
非阻塞多锁尝试 | 避免阻塞的多锁获取 |
更多推荐
所有评论(0)