第一百八十五章:互斥锁
大乘期涉及内核源码:
一
离开竞技场,林小源来到一座宁静的庭院。庭院中央有一间禅房,禅房的门紧闭着,门上挂着一块木牌:mutex。
门前排着三位修士,其中两位坐在地上闭目养神,一位在门口来回踱步。
"他们……在睡觉?"林小源惊讶地问。
门口踱步的修士转过头来。"我是mutex的守护者。没错,和自旋锁不同,mutex让获取不到锁的线程睡眠,而不是忙等待。等锁的人会进入等待队列,让出CPU,直到锁被释放时被唤醒。"
"这不是更省CPU吗?"
"省CPU,但有代价。"守护者指了指正在睡觉的两位修士,"睡眠和唤醒需要上下文切换,这本身有开销。如果临界区很短,上下文切换的开销可能比自旋等待还大。所以mutex适合持锁时间可能较长的场景。"
二
"但mutex有一个巧妙的优化——乐观自旋。"守护者的语气变得兴奋,"当一个线程尝试获取mutex时,它不会立刻睡眠。它会先自旋一小段时间,期望锁很快被释放。如果锁在自旋期间释放了,就避免了一次上下文切换。只有自旋一段时间后还没获取到锁,才会真正睡眠。"
"这不就是自旋锁和mutex的结合?"
"正是。"守护者点头,"乐观自旋利用了一个观察:mutex的持有者往往正在CPU上运行,很快就会释放锁。先自旋等一会儿,比立刻睡眠再被唤醒更高效。"
林小源想起了自旋锁和mutex的选择困境——乐观自旋恰好是两者的折中。
三
"mutex和自旋锁到底怎么选?"林小源问出了心中最大的疑问。
守护者竖起手指,一条条列举。"中断上下文,必须用自旋锁——中断不能睡眠。进程上下文,持锁时间短,用自旋锁;持锁时间可能长,用mutex。持锁时需要睡眠,必须用mutex——自旋锁禁止睡眠。"
"还有什么约束?"
"mutex不能递归获取——同一线程两次lock同一把mutex,死锁。mutex也不能在中断上下文使用——中断没有进程上下文,无法睡眠。"守护者从门上取下木牌递给林小源,"记住,选择锁的类型,是内核编程中最基本也最重要的决策之一。选错了,轻则性能下降,重则死锁。"
/*
* 互斥锁 (mutex):
*
* vs 自旋锁:
* 自旋锁: 忙等待,浪费 CPU
* mutex: 睡眠等待,节省 CPU
*
* 使用场景:
* 进程上下文
* 持锁时间可能较长
* 可能需要睡眠
*
* mutex 操作:
* mutex_lock() — 获取锁
* mutex_unlock() — 释放锁
* mutex_trylock() — 尝试获取
* mutex_lock_interruptible() — 可中断获取
*
* mutex 实现:
* owner — 持有者
* wait_list — 等待队列
* atomic 操作
* 乐观自旋
*
* 乐观自旋:
* 先自旋一段时间
* 如果锁很快释放,避免睡眠
* 如果锁长时间持有,再睡眠
*
* 约束:
* 不能在中断上下文使用
* 持锁时可以睡眠
* 不能递归获取
*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;
void *worker(void *arg) {
int id = *(int *)arg;
printf("线程 %d: 尝试获取锁\n", id);
pthread_mutex_lock(&mutex);
printf("线程 %d: 已获取锁\n", id);
shared_data++;
printf("线程 %d: shared_data = %d\n", id, shared_data);
printf("线程 %d: 释放锁\n", id);
pthread_mutex_unlock(&mutex);
return NULL;
}
printf("=== 互斥锁 — 进程上下文的选择 ===\n\n");
printf("--- 多线程互斥 ---\n");
pthread_t threads[3];
int ids[] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
printf("\n--- mutex vs 自旋锁 ---\n");
printf("自旋锁:\n");
printf(" 忙等待\n");
printf(" 浪费 CPU\n");
printf(" 适合短时间持锁\n\n");
printf("mutex:\n");
printf(" 睡眠等待\n");
printf(" 节省 CPU\n");
printf(" 适合长时间持锁\n\n");
printf("--- 乐观自旋 ---\n");
printf("实现:\n");
printf(" 先自旋一段时间\n");
printf(" 如果锁很快释放,避免睡眠\n");
printf(" 如果锁长时间持有,再睡眠\n\n");
printf("优势:\n");
printf(" 结合自旋和睡眠的优点\n");
printf(" 减少上下文切换\n\n");
printf("--- mutex 操作 ---\n");
printf("mutex_lock():\n");
printf(" 获取锁\n");
printf(" 如果获取不到,睡眠\n\n");
printf("mutex_unlock():\n");
printf(" 释放锁\n");
printf(" 唤醒等待者\n\n");
printf("mutex_trylock():\n");
printf(" 尝试获取\n");
printf(" 失败立即返回\n\n");
printf("mutex_lock_interruptible():\n");
printf(" 可中断获取\n");
printf(" 信号可以打断\n\n");
printf("--- 约束 ---\n");
printf("1. 不能在中断上下文使用\n");
printf(" 中断不能睡眠\n\n");
printf("2. 持锁时可以睡眠\n");
printf(" 与自旋锁不同\n\n");
printf("3. 不能递归获取\n");
printf(" 会导致死锁\n");
pthread_mutex_destroy(&mutex);#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
/*
* 互斥锁 (mutex):
*
* vs 自旋锁:
* 自旋锁: 忙等待,浪费 CPU
* mutex: 睡眠等待,节省 CPU
*
* 使用场景:
* 进程上下文
* 持锁时间可能较长
* 可能需要睡眠
*
* mutex 操作:
* mutex_lock() — 获取锁
* mutex_unlock() — 释放锁
* mutex_trylock() — 尝试获取
* mutex_lock_interruptible() — 可中断获取
*
* mutex 实现:
* owner — 持有者
* wait_list — 等待队列
* atomic 操作
* 乐观自旋
*
* 乐观自旋:
* 先自旋一段时间
* 如果锁很快释放,避免睡眠
* 如果锁长时间持有,再睡眠
*
* 约束:
* 不能在中断上下文使用
* 持锁时可以睡眠
* 不能递归获取
*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;
void *worker(void *arg) {
int id = *(int *)arg;
printf("线程 %d: 尝试获取锁\n", id);
pthread_mutex_lock(&mutex);
printf("线程 %d: 已获取锁\n", id);
shared_data++;
printf("线程 %d: shared_data = %d\n", id, shared_data);
printf("线程 %d: 释放锁\n", id);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
printf("=== 互斥锁 — 进程上下文的选择 ===\n\n");
printf("--- 多线程互斥 ---\n");
pthread_t threads[3];
int ids[] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
printf("\n--- mutex vs 自旋锁 ---\n");
printf("自旋锁:\n");
printf(" 忙等待\n");
printf(" 浪费 CPU\n");
printf(" 适合短时间持锁\n\n");
printf("mutex:\n");
printf(" 睡眠等待\n");
printf(" 节省 CPU\n");
printf(" 适合长时间持锁\n\n");
printf("--- 乐观自旋 ---\n");
printf("实现:\n");
printf(" 先自旋一段时间\n");
printf(" 如果锁很快释放,避免睡眠\n");
printf(" 如果锁长时间持有,再睡眠\n\n");
printf("优势:\n");
printf(" 结合自旋和睡眠的优点\n");
printf(" 减少上下文切换\n\n");
printf("--- mutex 操作 ---\n");
printf("mutex_lock():\n");
printf(" 获取锁\n");
printf(" 如果获取不到,睡眠\n\n");
printf("mutex_unlock():\n");
printf(" 释放锁\n");
printf(" 唤醒等待者\n\n");
printf("mutex_trylock():\n");
printf(" 尝试获取\n");
printf(" 失败立即返回\n\n");
printf("mutex_lock_interruptible():\n");
printf(" 可中断获取\n");
printf(" 信号可以打断\n\n");
printf("--- 约束 ---\n");
printf("1. 不能在中断上下文使用\n");
printf(" 中断不能睡眠\n\n");
printf("2. 持锁时可以睡眠\n");
printf(" 与自旋锁不同\n\n");
printf("3. 不能递归获取\n");
printf(" 会导致死锁\n");
pthread_mutex_destroy(&mutex);
return 0;
}道藏笔记
内核启示
互斥锁让等待的线程睡眠。
mutex vs 自旋锁:
- 自旋锁 — 忙等待,适合短时间
- mutex — 睡眠等待,适合长时间
乐观自旋:
- 先自旋,期望锁快速释放
- 长时间持有再睡眠
- 减少上下文切换
操作:
- mutex_lock — 获取
- mutex_unlock — 释放
- mutex_trylock — 尝试获取
- mutex_lock_interruptible — 可中断
约束:
- 不能在中断上下文使用
- 持锁时可以睡眠
mutex 是进程的选择——睡眠等待,节省 CPU。
互斥锁之试
进程上下文里持锁时间可能较长、等待者可以睡眠时,本章建议使用哪种锁?