Skip to content

第一百八十五章:互斥锁

大乘期

涉及内核源码:

离开竞技场,林小源来到一座宁静的庭院。庭院中央有一间禅房,禅房的门紧闭着,门上挂着一块木牌:mutex。

门前排着三位修士,其中两位坐在地上闭目养神,一位在门口来回踱步。

"他们……在睡觉?"林小源惊讶地问。

门口踱步的修士转过头来。"我是mutex的守护者。没错,和自旋锁不同,mutex让获取不到锁的线程睡眠,而不是忙等待。等锁的人会进入等待队列,让出CPU,直到锁被释放时被唤醒。"

"这不是更省CPU吗?"

"省CPU,但有代价。"守护者指了指正在睡觉的两位修士,"睡眠和唤醒需要上下文切换,这本身有开销。如果临界区很短,上下文切换的开销可能比自旋等待还大。所以mutex适合持锁时间可能较长的场景。"

"但mutex有一个巧妙的优化——乐观自旋。"守护者的语气变得兴奋,"当一个线程尝试获取mutex时,它不会立刻睡眠。它会先自旋一小段时间,期望锁很快被释放。如果锁在自旋期间释放了,就避免了一次上下文切换。只有自旋一段时间后还没获取到锁,才会真正睡眠。"

"这不就是自旋锁和mutex的结合?"

"正是。"守护者点头,"乐观自旋利用了一个观察:mutex的持有者往往正在CPU上运行,很快就会释放锁。先自旋等一会儿,比立刻睡眠再被唤醒更高效。"

林小源想起了自旋锁和mutex的选择困境——乐观自旋恰好是两者的折中。

"mutex和自旋锁到底怎么选?"林小源问出了心中最大的疑问。

守护者竖起手指,一条条列举。"中断上下文,必须用自旋锁——中断不能睡眠。进程上下文,持锁时间短,用自旋锁;持锁时间可能长,用mutex。持锁时需要睡眠,必须用mutex——自旋锁禁止睡眠。"

"还有什么约束?"

"mutex不能递归获取——同一线程两次lock同一把mutex,死锁。mutex也不能在中断上下文使用——中断没有进程上下文,无法睡眠。"守护者从门上取下木牌递给林小源,"记住,选择锁的类型,是内核编程中最基本也最重要的决策之一。选错了,轻则性能下降,重则死锁。"

c
/*
 * 互斥锁 (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);

道藏笔记

内核启示

互斥锁让等待的线程睡眠。

mutex vs 自旋锁:

  • 自旋锁 — 忙等待,适合短时间
  • mutex — 睡眠等待,适合长时间

乐观自旋:

  • 先自旋,期望锁快速释放
  • 长时间持有再睡眠
  • 减少上下文切换

操作:

  • mutex_lock — 获取
  • mutex_unlock — 释放
  • mutex_trylock — 尝试获取
  • mutex_lock_interruptible — 可中断

约束:

  • 不能在中断上下文使用
  • 持锁时可以睡眠

mutex 是进程的选择——睡眠等待,节省 CPU。


破关试炼

互斥锁之试

进程上下文里持锁时间可能较长、等待者可以睡眠时,本章建议使用哪种锁?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道