Skip to content

第一百八十六章:读写锁

大乘期

涉及内核源码:

离开mutex的庭院,林小源来到一座藏书阁。藏书阁共有三层,每层都摆满了书架,书架上堆满了玉简。

藏书阁的大门敞开着,十几位修士同时进出,自由翻阅玉简。但每当一位修士要往书架上添加新玉简时,所有正在翻阅的人都会停下脚步,退到一旁等待。

"这是读写锁。"一位白发管理员从书架后走出,"我是rw_semaphore的守护者。规则很简单——多个读者可以同时持锁,但写者必须独占。读者和写者互斥。"

"所以那些修士可以同时看书,但添加新书时必须等所有人都看完?"

"正是。"管理员点头,"这就是读写锁的精髓——读并发,写独占。在读多写少的场景中,比普通mutex高效得多,因为读者之间完全不阻塞。"

"不过这里有两座不同的藏书阁。"管理员指向左侧的铁门,"rwlock_t 是自旋读写锁,非 RT 内核里属于 spinning locks,适合不能睡眠的短临界区。右侧这座木门是 rw_semaphore,属于 sleeping locks,只能在可睡眠的任务上下文里使用。名字都叫读写,契约却不同。"

破关试炼

双阁初试

读写锁中,属于 sleeping lock、可让等待者睡眠的机制叫什么?

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

"但有一个问题。"管理员的表情变得忧虑,"写者饥饿。"

他指向门口。林小源看到,一位抱着新玉简的修士已经等了很久,但不断有新的读者涌入,他始终无法获取写锁。

"如果读者持续不断,写者可能永远获取不到锁。"管理员叹气,"解决方案是写者优先——当有写者在等待时,新的读者也必须等待,让写者先获取锁。这样保证写者不会饿死。"

"rw_semaphore是写者优先的吗?"

"是的。"管理员从怀中取出一块令牌,"rw_semaphore内部维护了写者等待的标记。一旦有写者排队,后续的读者会被阻塞,直到写者完成。这比纯粹的读写锁更公平。"

"但这条规矩也受 PREEMPT_RT 影响。"管理员把令牌翻到背面,露出一行小字,"非 RT 内核里的 rw_semaphore 实现是公平的,能避免写者饥饿。到了 PREEMPT_RT,它会映射到基于 rt_mutex 的实现,公平性发生变化:写者没法把优先级同时授给多个读者,所以一个低优先级读者被抢占时,可能拖住高优先级写者。"

"那 rwlock_t 呢?"

"类似。非 RT 内核里它是自旋锁,公平;PREEMPT_RT 上同样变成 rt_mutex 体系的一部分,读者和写者的优先级继承能力不对称。实时内核里,锁的名字常常不够,必须看它实际映射成什么。"

破关试炼

公平之试

非 PREEMPT_RT 内核中,rw_semaphore 文档说其实现是怎样的,从而避免 writer starvation?

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

"前辈,读写锁和RCU有什么关系?"林小源想起了之前的修炼。

管理员放下手中的玉简,认真说道:"RCU比读写锁更高效——读取完全无锁,连读者之间的同步都不需要。但RCU的写入开销更大——需要复制数据、等待宽限期。如果你的场景读写比例是99:1,RCU最合适;如果是80:20,读写锁可能更好。"

"所以选择取决于读写比例?"

"取决于读写比例、临界区大小、是否需要睡眠……"管理员掰着手指数,"没有万能的锁,只有最合适的锁。这就是内核编程的艺术——在正确性和性能之间找到平衡。"

"还要看写者是否能承受等待、读者是否能承受重试、数据里有没有指针生命周期问题。"管理员把三把钥匙放在桌上,"读写锁保护的是临界区;RCU保护的是读侧路径与回收时机;seqlock保护的是一致快照。它们看上去都在服务读多写少,真正约束却完全不同。"

林小源把三把钥匙收入袖中,终于明白:选锁不是按名字选,而是按上下文、数据形态和延迟目标选。

破关试炼

择锁之试

读写锁、RCU、seqlock 都可用于读多写少,但选择时必须先看上下文、数据形态和什么目标?

答对后才能继续滑动和进入下一章。
c
/*
 * 读写锁:
 *
 * 类型:
 *   rwlock_t — 自旋读写锁
 *   rw_semaphore — 读写信号量
 *
 * 操作:
 *   read_lock() — 获取读锁
 *   read_unlock() — 释放读锁
 *   write_lock() — 获取写锁
 *   write_unlock() — 释放写锁
 *
 * 规则:
 *   多个读者可以同时持锁
 *   写者独占
 *   读者和写者互斥
 *
 * 优势:
 *   读并发
 *   适合读多写少
 *
 * 问题:
 *   写者可能饥饿
 *   读者太多,写者无法获取锁
 *
 * 解决方案:
 *   写者优先
 *   读写公平
 */

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_data = 0;

void *reader(void *arg) {
    int id = *(int *)arg;

    pthread_rwlock_rdlock(&rwlock);
    printf("读者 %d: 读取 shared_data = %d\n", id, shared_data);
    pthread_rwlock_unlock(&rwlock);

    return NULL;
}

void *writer(void *arg) {
    int id = *(int *)arg;

    pthread_rwlock_wrlock(&rwlock);
    shared_data++;
    printf("写者 %d: 写入 shared_data = %d\n", id, shared_data);
    pthread_rwlock_unlock(&rwlock);

    return NULL;
}

printf("=== 读写锁 — 读多写少的选择 ===\n\n");

pthread_t threads[6];
int ids[] = {1, 2, 3, 4, 5, 6};

printf("--- 并发读 ---\n");
for (int i = 0; i < 3; i++) {
    pthread_create(&threads[i], NULL, reader, &ids[i]);
}
for (int i = 0; i < 3; i++) {
    pthread_join(threads[i], NULL);
}

printf("\n--- 写者 ---\n");
pthread_create(&threads[3], NULL, writer, &ids[3]);
pthread_join(threads[3], NULL);

printf("\n--- 读写锁规则 ---\n");
printf("读者:\n");
printf("  多个读者可以同时持锁\n");
printf("  读并发\n\n");
printf("写者:\n");
printf("  写者独占\n");
printf("  读者和写者互斥\n\n");

printf("--- 读写锁类型 ---\n");
printf("rwlock_t:\n");
printf("  自旋读写锁\n");
printf("  中断上下文使用\n\n");
printf("rw_semaphore:\n");
printf("  读写信号量\n");
printf("  进程上下文使用\n\n");

printf("--- 写者饥饿 ---\n");
printf("问题:\n");
printf("  读者太多\n");
printf("  写者无法获取锁\n\n");
printf("解决:\n");
printf("  写者优先\n");
printf("  新读者等待\n\n");

printf("--- 使用场景 ---\n");
printf("1. 路由表:\n");
printf("   读取频繁\n");
printf("   修改稀少\n\n");
printf("2. 配置数据:\n");
printf("   读取频繁\n");
printf("   偶尔更新\n\n");
printf("3. 文件系统元数据:\n");
printf("   读取频繁\n");
printf("   修改较少\n");

pthread_rwlock_destroy(&rwlock);

道藏笔记

内核启示

读写锁区分读和写操作。

规则:

  • 多个读者可以同时持锁
  • 写者独占
  • 读者和写者互斥

类型:

  • rwlock_t — 自旋读写锁
  • rw_semaphore — 读写信号量
  • PREEMPT_RT 下二者语义和公平性可能变化

写者饥饿:

  • 读者太多,写者等待
  • 解决:写者优先

使用场景:

  • 读多写少
  • 路由表、配置数据

读写锁是精细控制——读并发,写独占。


破关试炼

读写锁之试

读多写少场景中,读者不想被写者阻塞时,本章最终指向哪套机制?

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

以修仙之名,悟内核之道