操作系统学习(七)——互斥
一、什么是互斥?
在操作系统和并发编程中,互斥(Mutual Exclusion) 是为了防止多个线程或进程同时访问共享资源(如内存、文件、变量等)所采取的一种机制。
它是实现并发安全(Concurrency Safety)的核心。
互斥的本质是:
同一时间只允许一个执行单元(线程或进程)访问某个临界资源(Critical Resource)。
例如:两个线程不能同时写同一个文件或同时修改一个变量,否则可能引发数据冲突或系统崩溃。
二、互斥的关键概念
概念 | 描述 |
---|---|
临界区(Critical Section) | 对共享资源进行访问的代码区段 |
互斥锁(Mutex) | 控制对临界区访问的锁机制 |
锁(Lock)/解锁(Unlock) | 获得/释放资源访问权 |
同步 | 协调多个线程执行顺序 |
三、互斥的实现
1. 软件实现(早期)
如经典的 Peterson算法、Dekker算法 等,但效率不高,仅用于教学。
2. 硬件指令支持(低级原语)
利用原子指令(指那些在执行过程中不会被其他操作干扰,要么完全执行,要么完全不执行的指令)实现:
Test-and-Set
;Compare-and-Swap(CAS)
;Load-Link / Store-Conditional
。
这些原语可以在硬件层面保证:多个线程对一个变量的读-改-写是原子的。
3. 操作系统提供的机制
类型 | 平台 | 说明 |
---|---|---|
互斥锁(pthread_mutex_t ) | Linux / POSIX | 最常用的线程锁机制 |
关键段(CRITICAL_SECTION ) | Windows | 类似互斥锁,但更轻量 |
std::mutex | C++11标准 | 封装操作系统互斥机制 |
synchronized | Java | 自动加锁关键字 |
threading.Lock | Python | Python GIL 下线程互斥 |
四、互斥的使用
互斥锁的使用流程:
[创建] ──▶ [上锁] ──▶ [临界区] ──▶ [解锁] ──▶ [销毁]↑ ↓仅允许一个线程进入
1. C语言 pthread
互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* task(void* arg)
{pthread_mutex_lock(&mutex); // 加锁// 访问临界区资源pthread_mutex_unlock(&mutex); // 解锁return NULL;
}
2. C++11 std::mutex
:
#include <mutex>
std::mutex mtx;void task()
{mtx.lock(); // 加锁// 临界区mtx.unlock(); // 解锁
}
或使用更安全的 RAII 风格:
std::lock_guard<std::mutex> lock(mtx);
// 自动加解锁
3. Python 中互斥锁:
import threading
lock = threading.Lock()def task():with lock:# 临界区代码pass
五、互斥存在的问题
问题 | 描述 |
---|---|
死锁(Deadlock) | 多线程互相等待对方释放锁,永远阻塞 |
饥饿(Starvation) | 某些线程始终抢不到锁,无法执行 |
优先级反转 | 高优线程被低优线程持锁阻塞 |
性能下降 | 锁竞争严重时会导致上下文频繁切换 |
六、死锁
死锁指两个或多个进程(线程)在执行过程中,因为争夺资源而造成一种互相等待的现象,若无外力干预,它们都将无法推进。
死锁示意:
线程A 线程B
持有锁1 持有锁2
请求锁2 请求锁1
→ 相互等待形成死锁
1. 死锁的条件(Prevention)
死锁的四个必要条件(Coffman 条件):
(1)互斥条件(Mutual Exclusion):至少有一个资源只能被一个进程使用。
(2)占有和等待条件(Hold and Wait):已持有资源的进程在等待获取其他资源时不释放已占资源。
(3)不可抢占条件(No Preemption):已分配资源不能被系统强制回收,只能由进程主动释放。
(4)环路等待条件(Circular Wait):存在一个进程资源环路:进程A等待B占用的资源,B又等待A占用的资源。
只要四个条件同时满足,就可能发生死锁。
2. 预防死锁
在程序设计时破坏死锁必要条件之一:
- 禁止请求与保持(加锁前先申请全部资源);
- 设置资源获取顺序;
- 允许资源抢占(如数据库)。
3. 避免死锁(Avoidance)
通过动态检查确保系统永远不进入死锁状态:
银行家算法:资源分配前检查是否仍处于“安全状态”
4. 死锁的检测与恢复(Detection + Recovery)
允许死锁发生,定期检测资源图中的“环”,发现死锁后终止或回滚某些进程。
5. 忽略死锁(鸵鸟策略)
部分系统(如 Linux)直接忽略死锁,只要概率极低或后果不严重。
七、读写锁
读写锁(Read-Write Lock,又称共享-互斥锁)是一种比互斥锁更高效的同步机制,适用于“读多写少”的并发场景。
核心思想:
- 多个线程可同时读共享资源(不互斥);
- 写操作必须独占资源(与其他读写线程都互斥)。
1. POSIX C 语言:pthread_rwlock_t
#include <pthread.h>
#include <stdio.h>pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_data = 0;void* reader(void* arg)
{pthread_rwlock_rdlock(&rwlock); // 读锁printf("Reader: %d\n", shared_data);pthread_rwlock_unlock(&rwlock); // 解锁return NULL;
}void* writer(void* arg)
{pthread_rwlock_wrlock(&rwlock); // 写锁shared_data += 1;printf("Writer updated to %d\n", shared_data);pthread_rwlock_unlock(&rwlock); // 解锁return NULL;
}
API 对比表(POSIX)
操作 | 函数 |
---|---|
初始化 | pthread_rwlock_init() |
读加锁 | pthread_rwlock_rdlock() |
写加锁 | pthread_rwlock_wrlock() |
尝试读锁 | pthread_rwlock_tryrdlock() |
尝试写锁 | pthread_rwlock_trywrlock() |
解锁 | pthread_rwlock_unlock() |
销毁 | pthread_rwlock_destroy() |
2. C++17 及以上(std::shared_mutex
)
#include <shared_mutex>
#include <thread>
#include <iostream>std::shared_mutex rwlock;
int shared_data = 0;void reader()
{std::shared_lock<std::shared_mutex> lock(rwlock); // 读锁std::cout << "Reader: " << shared_data << "\n";
}void writer()
{std::unique_lock<std::shared_mutex> lock(rwlock); // 写锁shared_data += 1;std::cout << "Writer updated to " << shared_data << "\n";
}
3. Python(标准库无原生读写锁,需要自实现或第三方库)
简易实现:
import threadingclass ReadWriteLock:def __init__(self):self.lock = threading.Lock()self.readers = 0self.read_lock = threading.Lock()def acquire_read(self):with self.read_lock:self.readers += 1if self.readers == 1:self.lock.acquire()def release_read(self):with self.read_lock:self.readers -= 1if self.readers == 0:self.lock.release()def acquire_write(self):self.lock.acquire()def release_write(self):self.lock.release()
八、适当使用互斥的原则
- 锁粒度适中:不要锁太大范围(性能低),也不能锁太小(频繁加锁开销高);
- 尽量减少锁嵌套:避免死锁;
- 读多写少用读写锁(
rwlock
); - 避免忙等待:使用条件变量代替轮询;
- 优先使用语言内建机制(如 C++
std::lock_guard
,Pythonwith
)。