news 2026/10/2 21:10:16

Linux --读者写者问题、读写锁与自旋锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux --读者写者问题、读写锁与自旋锁

为什么需要读者写者问题?

在多线程编程中,同步是一个永恒的话题。我们之前接触过生产者-消费者问题,它描述的是:生产者往缓冲区放数据,消费者从缓冲区取数据,两者需要互斥地访问缓冲区,同时还要在缓冲区空/满时进行等待和唤醒。

而读者写者问题是另一类经典的同步问题,它的场景是:

  • 有一块共享数据(比如一个文件、一个数据库、一块内存)。

  • 多个读者可以同时读取这块数据,因为读操作不会改变数据,所以读者之间不需要互斥。

  • 写者在写入数据时,必须独占访问,因为写操作会改变数据,如果此时有其他读者或写者在访问,就会导致数据不一致。

  • 写者与读者之间、写者与写者之间必须互斥。

简单来说:读共享,写独占。

读者写者 vs 生产者消费者

对比项生产者-消费者读者-写者
核心矛盾缓冲区空/满时的等待与唤醒读与写、写与写之间的互斥
互斥关系生产者与消费者互斥访问缓冲区写者与所有人互斥,读者之间不互斥
同步关系生产者等待缓冲区有空位,消费者等待缓冲区有数据读者等待写者离开,写者等待所有读者离开
典型场景消息队列、任务队列文件读写、数据库读写、配置中心

重点理解:读者写者问题的核心在于如何协调读者和写者之间的同步,使得读操作可以并发,写操作必须独占,同时还要避免某一方“饿死”。

读者写者问题的伪代码

公共部分

uint32_t reader_count = 0; // 当前正在读取的读者数量 lock_t count_lock; // 保护 reader_count 的锁 lock_t writer_lock; // 写者锁,读者和写者共享
  • reader_count:记录当前有多少个读者正在读。

  • count_lock:因为多个读者可能同时修改reader_count,所以需要一把锁来保护它。

  • writer_lock:写者需要持有的锁,同时第一个读者进入时也要持有它,最后一个读者离开时释放它。

Reader(读者)

// 加锁 lock(count_lock); if (reader_count == 0) lock(writer_lock); // 第一个读者,锁住写者锁 ++reader_count; unlock(count_lock); // read; // 解锁 lock(count_lock); --reader_count; if (reader_count == 0) unlock(writer_lock); // 最后一个读者,释放写者锁 unlock(count_lock);

逻辑解读:

  1. 读者先锁住count_lock,然后检查自己是不是第一个读者。

  2. 如果是第一个读者(reader_count == 0),说明当前没有读者在读,但可能有写者在写,所以需要获取writer_lock。如果写者正在写,读者会在这里阻塞,直到写者释放writer_lock。

  3. 然后reader_count++,表示自己开始读了,释放count_lock。

  4. 读取数据。

  5. 读完后,再次锁住count_lock,reader_count--。

  6. 如果自己是最后一个读者(reader_count == 0),说明所有读者都离开了,此时需要释放writer_lock,让等待的写者可以进入。

  7. 释放count_lock。

关键点:第一个读者负责“锁住”写者,最后一个读者负责“释放”写者。中间的读者只是简单地增加/减少计数,不会触碰writer_lock。

Writer(写者)

lock(writer_lock); // write unlock(writer_lock);

逻辑解读:

  1. 写者直接尝试获取writer_lock。

  2. 如果此时有读者正在读(writer_lock被第一个读者持有),写者会阻塞。

  3. 如果此时有其他写者在写,写者也会阻塞。

  4. 获取到锁后,独占写入。

  5. 写完释放锁。

注意:这个伪代码实现的是读者优先策略。因为只要有一个读者持有writer_lock,后续的读者都可以直接进入(它们只需要count_lock),而写者必须等待所有读者离开。如果读者源源不断,写者可能永远等待,即写者饥饿。

读写锁(pthread_rwlock)

在实际编程中,我们不需要自己手动实现上述逻辑,POSIX 提供了读写锁(pthread_rwlock_t),它封装了读者写者的同步机制。

读写锁的行为

当前锁状态读锁请求写锁请求
无锁可以可以
读锁可以阻塞
写锁阻塞阻塞

总结:写独占,读共享,读锁优先级高(默认)。

读写锁的接口

初始化与销毁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • attr通常传NULL,表示使用默认属性。

  • 使用前必须初始化,使用后必须销毁。

加锁与解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 读锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 写锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 解锁
  • 读锁:多个线程可以同时持有读锁。

  • 写锁:同一时刻只能有一个线程持有写锁,且此时不能有读锁。

  • 解锁:无论是读锁还是写锁,都用同一个unlock。

设置读写优先级
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);

pref有三种选择:

选项含义
PTHREAD_RWLOCK_PREFER_READER_NP读者优先(默认),可能导致写者饥饿
PTHREAD_RWLOCK_PREFER_WRITER_NP写者优先,但目前有 BUG,表现和读者优先一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP写者优先,但写者不能递归加锁

注意:写者优先可以缓解写者饥饿,但可能导致读者饥饿。实际使用时需要根据场景权衡。

读者优先 vs 写者优先

  • 读者优先:只要有读者在读,后续读者可以直接进入,写者必须等待所有读者离开。可能导致写者饥饿。

  • 写者优先:一旦有写者到达,后续读者会被阻塞,直到写者完成。可能导致读者饥饿。

选择建议:

  • 如果读操作非常频繁,写操作很少,且写者饥饿不是问题,可以用读者优先。

  • 如果写操作也很重要,不能长时间等待,可以用写者优先(注意PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP)。

自旋锁(Spinlock)

什么是自旋锁?

自旋锁是一种忙等待的锁。当一个线程尝试获取自旋锁但锁已被占用时,它不会进入休眠,而是在一个循环中不断检查锁是否可用。一旦锁被释放,线程立即获取。

类比:你去上厕所,发现门锁着,你不会回去睡觉,而是站在门口一直问“好了没?好了没?”,直到里面的人出来。

自旋锁的原理

自旋锁通常使用一个原子标志位来表示锁的状态:

  • false:锁可用。

  • true:锁已被占用。

获取锁时,使用CAS(Compare-And-Swap)原子操作:

while (atomic_flag_test_and_set(&spinlock)) { // 忙等待 }
  • atomic_flag_test_and_set:如果标志位为false,则设置为true并返回false(表示获取成功);如果为true,则返回true(表示获取失败)。

  • 释放锁:atomic_flag_clear(&spinlock);将标志位设为false。

自旋锁的优缺点

优点:

  1. 低延迟:不会让线程休眠,避免了线程切换的开销。

  2. 减少系统调度开销:等待锁的线程不会被阻塞,不需要上下文切换。

缺点:

  1. CPU 资源浪费:如果锁持有时间较长,等待线程会一直自旋,浪费 CPU。

  2. 可能引起活锁:多个线程同时自旋,如果没有退避策略,可能都无法进入临界区。

使用场景

  • 短暂等待:锁被占用的时间很短,比如简单的计数器操作。

  • 多线程锁使用:通常用于系统底层,同步多个 CPU 对共享资源的访问。

  • 不可睡眠的场景:比如中断处理程序中,不能休眠,只能用自旋锁。

Linux 提供的自旋锁系统调用

#include <pthread.h> int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock); int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy(pthread_spinlock_t *lock);
  • pshared:PTHREAD_PROCESS_PRIVATE表示线程间共享,PTHREAD_PROCESS_SHARED表示进程间共享。

互斥锁 vs 自旋锁 vs 读写锁

锁类型行为适用场景缺点
互斥锁(Mutex)获取失败时休眠,释放时唤醒锁持有时间较长,竞争不激烈上下文切换开销
自旋锁(Spinlock)获取失败时忙等待锁持有时间极短,多核环境CPU 浪费,可能活锁
读写锁(Rwlock)读共享,写独占多读少写可能读者/写者饥饿

选择原则:

  • 如果临界区执行时间短,且 CPU 多核,用自旋锁。

  • 如果临界区执行时间长,或者单核,用互斥锁。

  • 如果读多写少,用读写锁。

读者写者问题的变种

  1. 读者优先:只要有一个读者在读,后续读者直接进入,写者等待。

  2. 写者优先:一旦有写者等待,后续读者阻塞,写者优先进入。

  3. 公平竞争:读者和写者按到达顺序竞争,避免饥饿。

读写锁的实现细节

读写锁的实现通常需要一个计数器和两个条件变量(或信号量):

  • reader_count:当前读者数量。

  • writer_count:当前写者数量(通常为 0 或 1)。

  • mutex:保护计数器。

  • read_cond:读者等待的条件变量。

  • write_cond:写者等待的条件变量。

读者进入时:

  • 如果写者正在写,等待。

  • 否则,reader_count++,进入读。

写者进入时:

  • 如果读者正在读或写者正在写,等待。

  • 否则,进入写。

自旋锁的优化

  • 退避策略:自旋失败后,暂停一段时间再重试,减少 CPU 浪费。

  • 队列自旋锁:每个线程在一个队列中等待,避免所有线程同时自旋。

  • 自适应自旋锁:根据历史成功率动态调整自旋时间。

实际应用中的建议

  • 避免锁的嵌套:容易导致死锁。

  • 锁的粒度要小:只保护必要的临界区。

  • 读写锁并不是解决所有并发问题的万能工具:如果写操作也很频繁,读写锁可能不如互斥锁。

  • 自旋锁不要用于单核:单核自旋没有意义,因为自旋的线程占着 CPU,持有锁的线程无法运行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 21:10:16

nVisual产品体系关系说明

nVisual 产品体系关系说明本文基于 nVisual 官网「在线工具」页面&#xff08;https://www.nvisual.com/tool/ &#xff09;与产品体系关系图&#xff08;nvisual-data-modified.svg&#xff09;整理&#xff0c;用于说明 nVisual 各产品之间的定位、分工与数据流向。一、一句话…

作者头像 李华
网站建设 2026/10/2 21:08:31

护照阅读器,融合多光谱成像、AI OCR与RFID芯片解密,实现秒级通关

每到出行旺季&#xff0c;国际机场出入境大厅总是挤满排队的旅客。过去人工核验护照&#xff0c;工作人员要肉眼分辨证件真伪、手动录入身份信息&#xff0c;遇上多国语言、磨损老旧护照&#xff0c;耗时更长。如今不少自助通道&#xff0c;旅客只需要把护照轻放在设备上&#…

作者头像 李华
网站建设 2026/10/2 21:07:42

【SQLite】从零开始学数据库索引——用 EXPLAIN 找出慢查询

【SQLite】从零开始学数据库索引——用 EXPLAIN 找出慢查询 订单列表只显示某个用户最近二十条记录&#xff0c;表里却存着所有人的订单。数据少时&#xff0c;这条 SQL 几乎没有存在感&#xff1b;数据一多&#xff0c;列表开始等待。有人建议给用户编号加索引&#xff0c;也有…

作者头像 李华
网站建设 2026/10/2 21:06:53

RVMedia源码修复版在Delphi 12.3 Win64下的编译与摄像头预览实践

简介&#xff1a;RVMedia v9.3 完整源码修复版是一套面向 Delphi、CBuilder 与 Lazarus 开发者的视频音频流组件&#xff0c;专门用于在 Delphi 12.3 64 位环境下快速集成摄像头接入、画面预览、语音对讲、云端推流、本地录制及云台控制等功能。该版本修复了原有组件在 Delphi …

作者头像 李华
网站建设 2026/10/2 21:06:31

单视频三维重构支撑应急指挥从经验决策向沙盘推演升级

摘要突发事件应急指挥具备态势突变、风险耦合、时序紧迫、决策容错率极低的典型特征&#xff0c;传统应急指挥高度依赖指挥员临场经验、人工踏勘研判与固化预案执行&#xff0c;存在态势感知片面、风险预判主观、力量调度粗放、决策试错成本高、动态适配性弱等突出短板&#xf…

作者头像 李华
网站建设 2026/10/2 21:04:27

《P17017 [GESP202606 八级] 堆石子》

题目描述有 m 堆石子&#xff0c;编号为 1,2,⋯,m&#xff0c;其石子数量分别记为 a1​,a2​,⋯,am​。现在要求第 1 堆石子恰有 n 个&#xff08;即 a1​n&#xff09;&#xff0c;并且此后每堆石子的数量严格小于前一堆&#xff0c;即 ai​<ai−1​ (2≤i≤m)。此外&#…

作者头像 李华