【多线程】互斥锁(Mutex)是什么?
【多线程】互斥锁(Mutex)是什么?
本文来自于我关于多线程系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】信号量(Semaphore)是什么?
3.【多线程】信号量(Semaphore)常见的应用场景
互斥锁(Mutex) 是多线程编程中最基本、最重要的同步工具之一。它的名字揭示了它的核心功能:互斥(Mutual Exclusion)。
一句话概括
互斥锁就像是一个只有一个钥匙的厕所。一个人(线程)进去后,会把门锁上(加锁),手里拿着唯一的钥匙。其他想用厕所的人(线程)必须等在门口(阻塞),直到里面的人出来(解锁)并把钥匙交给下一个人才行。
核心概念
互斥锁用于保护临界区(Critical Section)。临界区是一段代码,在这段代码中会访问共享资源(如全局变量、文件、设备等),这段代码不能被多个线程同时执行,否则可能导致数据不一致、逻辑错误等问题。
互斥锁提供了两个基本操作:
-
加锁(Lock):
- 一个线程在进入临界区之前,必须尝试获取锁。
- 如果锁当前是空闲的(未被任何线程持有),那么该线程成功获得锁,并进入临界区执行。
- 如果锁已经被其他线程持有,那么当前线程会被阻塞(即进入等待状态),直到锁被释放。
-
解锁(Unlock):
- 一个线程在离开临界区时,必须释放它持有的锁。
- 这个操作会使锁变为空闲状态。如果有其他线程正在等待这个锁,操作系统会唤醒其中一个等待的线程,该被唤醒的线程随后可以获得锁并继续执行。
一个生动的比喻:会议室与白板
想象一个团队共用一间会议室和一块白板(共享资源)进行头脑风暴。
- 互斥锁就像是会议室门上的一个状态牌,只有两种状态:“空闲”或“使用中”。
- 线程就像是想要使用会议室的团队成员。
- 临界区就是使用会议室和白板进行讨论的整个过程。
工作流程如下:
- Alice想用会议室。她看到状态是“空闲”,于是把它翻到“使用中”,然后进去开始在白板上写写画画。(加锁成功)
- Bob也想用会议室。他来到门口,看到状态是“使用中”,于是他只好在门口等待。(尝试加锁,但被阻塞)
- Alice讨论完了,擦干净白板,走出会议室,并把状态牌翻回“空闲”。(解锁)
- Bob看到状态变为“空闲”,立即将牌子翻到“使用中”,然后进入会议室使用。(被唤醒并加锁成功)
这个机制确保了同一时间只有一个人能在白板上写字,避免了 ideas 被混乱地写在一起。
关键特性
-
所有权(Ownership):
- 这是互斥锁与信号量最关键的区别。互斥锁有所有者的概念,即哪个线程加了锁,就必须由同一个线程来解锁。其他线程不能帮你解锁。
- 这个特性避免了许多复杂的同步错误。
-
原子性(Atomic Operations):
- 检查锁状态和获取锁这两个动作是合并为一个原子操作完成的。这意味着在这个操作过程中不会被其他线程打断,从而保证了即使多个线程同时尝试加锁,也只有一个能成功。
-
排队(Queuing):
- 当多个线程等待同一个锁时,它们通常会进入一个等待队列。当锁被释放时,操作系统会从队列中选一个线程(具体的挑选策略可能因系统而异,如先进先出FIFO或优先级)来唤醒它。
代码示例(伪代码)
让我们看一个经典的“银行账户取款”例子。如果没有锁,多个线程同时取款会导致余额错误。
// 伪代码// 1. 定义一个全局的互斥锁
Mutex lock;
// 2. 共享资源:账户余额
int account_balance = 1000;void withdraw_money(int amount) {// 3. 进入临界区前:加锁lock.lock(); // --- 临界区开始 ---// 以下操作是安全的,不会被其他线程打断if (account_balance >= amount) {// 模拟一些操作时间,如果没有锁,问题更容易在这里暴露sleep(1); account_balance = account_balance - amount;print("取款成功,余额为:", account_balance);} else {print("余额不足!");}// --- 临界区结束 ---// 4. 离开临界区:解锁lock.unlock();
}// 主程序
main() {Thread thread1 = create_thread(withdraw_money, 800); // 线程1取800Thread thread2 = create_thread(withdraw_money, 800); // 线程2也取800thread1.join();thread2.join();
}
可能的结果:
- 无锁情况:两个线程都检查余额(1000>800),都认为可以取款,最终余额可能变为 -600,这是严重的错误。
- 有锁情况:
- 线程1先获得锁,执行完整个取款流程(余额变为200)后释放锁。
- 线程2然后获得锁,检查余额(200 < 800),打印“余额不足”。
- 结果是正确的。
互斥锁(Mutex) vs. 信号量(Semaphore)
为了更清晰地理解,我们再对比一下:
特性 | 互斥锁 (Mutex) | 二进制信号量 (Binary Semaphore, =1) |
---|---|---|
核心目的 | 实现互斥 | 发信号、同步(也可用于互斥) |
所有权 | 有。锁的持有者必须负责解锁。 | 无。任何线程都可以执行 V() (释放)。 |
行为 | 保护一个临界区,让线程“串行”访问。 | 像一个开关,通知一个线程某个事件已发生。 |
复杂度/风险 | 简单直观,不易出错。 | 更灵活,但也更容易误用(如错误的释放顺序)。 |
简单记:互斥锁是“谁上锁,谁解锁”,用于保护资源;信号量是“谁都可以发信号”,用于协调工作。
总结
- 互斥锁是什么? 一个提供互斥功能的同步原语。
- 为什么需要它? 为了保护共享资源,防止多个线程同时访问造成竞争条件(Race Condition)。
- 怎么工作? 通过
lock()
和unlock()
两个操作,确保一次只有一个线程能进入被保护的临界区。 - 关键特点? 所有权概念,即加锁和解锁必须是同一个线程。
它是一种“防御性”的编程机制,是构建线程安全程序的基础。