news 2026/10/3 22:29:59

Linux 线程同步:读写锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 线程同步:读写锁

Linux 线程同步:读写锁

课程:尚硅谷《嵌入式 Linux 应用层开发》第 4 章线程处理
依据:2026-09-29 19:30 录音转写,整理到15:02;课程 PDF 第 150—158 页。
本节边界:讲清读写锁原理、基础 API、未加写锁的错误示例、加写锁后的修正,以及读写线程执行顺序实验的准备。第 158 页开始的“写饥饿”对应录音 17:56 以后,留到下一节。

1. 为什么互斥锁之后还要学习读写锁

互斥锁不区分读和写。只要一个线程持有互斥锁,其他线程都要等待。

但很多共享数据具有“读得多、写得少”的特点:

  • 多个线程只读取同一份稳定数据时,可以并发执行;
  • 写线程修改数据时,必须独占;
  • 写入期间不能让读线程看到一半旧、一半新的状态。

读写锁(read-write lock)用两种加锁方式表达这种规则:

读锁:共享占用,允许多个读者并发 写锁:独占占用,只允许一个写者进入

2. 读锁与写锁的兼容关系

当前状态新读者申请读锁新写者申请写锁
没有线程持锁可以获得可以获得
一个或多个线程持有读锁通常可继续获得读锁必须等待全部读锁释放
一个线程持有写锁必须等待必须等待

“通常可继续获得读锁”还受调度策略以及是否已有写者等待等因素影响,不能据此假定读者永远优先。

读

否

是

写

否

是

线程申请访问共享数据

读还是写

当前是否有写者持锁

取得读锁

阻塞等待

读取共享数据

释放读锁

是否有任何读者或写者持锁

取得写锁

修改共享数据

释放写锁

3. “多个线程同时读没问题”的适用条件

录音用“大家一起看已经写好的黑板”解释读锁。这个比喻成立需要满足:

  • 读者只读取,不修改;
  • 对象在读取期间仍然存在;
  • 没有写者绕过锁修改;
  • 所有读者和写者使用同一把读写锁。

如果共享数据可能被写线程修改,读线程也必须获取读锁。否则读线程可能与写线程形成数据竞争,或看到不一致的组合状态。

4.pthread_rwlock_t的使用原则

读写锁类型声明为:

#include<pthread.h>pthread_rwlock_trwlock;

教材第 151 页展示了 glibc 内部的联合体定义。应用程序应把pthread_rwlock_t当作不透明类型:

  • 只通过<pthread.h>使用;
  • 不访问内部成员;
  • 不复制读写锁对象;
  • 给相关函数传入同一个对象的地址。

5. 初始化与销毁

5.1 静态初始化

默认属性下,可以在定义时初始化:

staticpthread_rwlock_trwlock=PTHREAD_RWLOCK_INITIALIZER;

这种方式不再调用pthread_rwlock_init()。

5.2 动态初始化

pthread_rwlock_trwlock;interror=pthread_rwlock_init(&rwlock,NULL);

第二个参数是属性对象。传NULL表示使用默认属性。

5.3 销毁

interror=pthread_rwlock_destroy(&rwlock);

销毁前必须确认:

  • 所有使用它的线程已经结束或不再访问它;
  • 当前没有读者或写者持锁;
  • 不会再有线程等待这把锁。

静态初始化只表示不需要调用pthread_rwlock_init()。如果锁与进程同寿命,程序退出时系统会回收进程资源;如果对象有独立、可重复的生命周期,则应在安全时显式销毁。

6. 本节五个核心 API

intpthread_rwlock_init(pthread_rwlock_t*restrict rwlock,constpthread_rwlockattr_t*restrict attr);intpthread_rwlock_rdlock(pthread_rwlock_t*rwlock);intpthread_rwlock_wrlock(pthread_rwlock_t*rwlock);intpthread_rwlock_unlock(pthread_rwlock_t*rwlock);intpthread_rwlock_destroy(pthread_rwlock_t*rwlock);
函数作用获取不到时
pthread_rwlock_rdlock获取读锁有写者阻挡时等待
pthread_rwlock_wrlock获取写锁只要还有读者或写者,就等待
pthread_rwlock_unlock释放当前线程持有的一次读锁或写锁不适用

同一把读写锁的读锁和写锁都用pthread_rwlock_unlock()释放。

Pthreads 函数成功返回0,否则返回错误号。基础检查写法:

interror=pthread_rwlock_rdlock(&rwlock);if(error!=0){fprintf(stderr,"pthread_rwlock_rdlock: %s\n",strerror(error));}

7. 第一个实验:写操作没有加锁

教材第 152—154 页故意让两个写线程修改同一个变量,却不给写操作加锁:

staticintshared_data=0;staticvoid*writer_without_lock(void*arg){inttemp=shared_data+1;sleep(1);shared_data=temp;printf("%s 完成写入,shared_data = %d\n",(char*)arg,shared_data);returnNULL;}

sleep(1)是为了放大两个写线程发生交错的机会:

步骤写线程 1写线程 2shared_data
1读取 0,算出temp=10
2睡眠读取 0,算出temp=10
3睡眠0
4写回 11
5写回 11

预期加两次得到 2,实际可能得到 1。这与上一节number++的丢失更新本质相同。

7.1 只有读线程加读锁,为什么仍然错误

教材错误示例中的读线程这样写:

pthread_rwlock_rdlock(&rwlock);printf("shared_data = %d\n",shared_data);pthread_rwlock_unlock(&rwlock);

读线程之间可以互相协调,但两个写线程完全没有使用这把锁。读写锁只有在所有访问者共同遵守时才有效。

读者使用 rwlock + 写者绕过 rwlock ↓ 同步协议不完整

8. 修正实验:写线程获取写锁

教材第 154—156 页给写操作补上写锁:

staticvoid*lock_writer(void*arg){pthread_rwlock_wrlock(&rwlock);inttemp=shared_data+1;sleep(1);/* 仅为演示持锁期间其他线程会等待 */shared_data=temp;printf("%s 完成写入,shared_data = %d\n",(char*)arg,shared_data);pthread_rwlock_unlock(&rwlock);returnNULL;}

执行过程变成:

写线程 2读写锁写线程 1写线程 2读写锁写线程 1阻塞等待wrlock成功wrlockshared_data 由 0 变 1unlock获得写锁shared_data 由 1 变 2unlock

两个写线程不再同时进入临界区,最终值稳定为 2。

9. 基础完整练习:两个写者和三个读者

下面的例子保留录音中的主要现象,同时补上返回值检查。读者看到 0、1 或 2 都有可能,因为调度顺序不确定;但每次读取都发生在读锁保护下,最终值应为 2。

#include<pthread.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>staticintshared_data=0;staticpthread_rwlock_trwlock=PTHREAD_RWLOCK_INITIALIZER;staticvoidcheck_pthread(interror,constchar*operation){if(error!=0){fprintf(stderr,"%s: %s\n",operation,strerror(error));exit(EXIT_FAILURE);}}staticvoid*reader(void*arg){constchar*name=arg;check_pthread(pthread_rwlock_rdlock(&rwlock),"pthread_rwlock_rdlock");printf("%s 读取:%d\n",name,shared_data);sleep(1);/* 演示多个读者可以同时持有读锁 */check_pthread(pthread_rwlock_unlock(&rwlock),"pthread_rwlock_unlock");returnNULL;}staticvoid*writer(void*arg){constchar*name=arg;check_pthread(pthread_rwlock_wrlock(&rwlock),"pthread_rwlock_wrlock");++shared_data;printf("%s 写入:%d\n",name,shared_data);check_pthread(pthread_rwlock_unlock(&rwlock),"pthread_rwlock_unlock");returnNULL;}intmain(void){pthread_tthreads[5];check_pthread(pthread_create(&threads[0],NULL,reader,"reader1"),"pthread_create");check_pthread(pthread_create(&threads[1],NULL,reader,"reader2"),"pthread_create");check_pthread(pthread_create(&threads[2],NULL,writer,"writer1"),"pthread_create");check_pthread(pthread_create(&threads[3],NULL,reader,"reader3"),"pthread_create");check_pthread(pthread_create(&threads[4],NULL,writer,"writer2"),"pthread_create");for(inti=0;i<5;++i){check_pthread(pthread_join(threads[i],NULL),"pthread_join");}printf("最终值:%d\n",shared_data);check_pthread(pthread_rwlock_destroy(&rwlock),"pthread_rwlock_destroy");returnEXIT_SUCCESS;}

编译运行:

gcc-Wall-Wextra-pthreadrwlock_basic.c-orwlock_basic ./rwlock_basic

9.1 观察重点

多运行几次,输出顺序可能改变:

  • 先创建的线程不保证先执行;
  • 多个读者可以同时持有读锁;
  • 写者必须等所有当前读者释放;
  • 写者持锁时,新的读者和其他写者都不能进入;
  • 所有线程结束后,最终值为 2。

10. 为什么创建顺序不等于执行顺序

教材第 156—158 页把第二个写线程放到多个读线程之间创建,用来观察读写顺序:

创建 writer1 创建 reader1、reader2、reader3 创建 writer2 创建 reader4、reader5、reader6

这只是向系统提交线程的先后顺序。线程何时真正获得 CPU、何时开始申请锁,由调度器和当时的锁状态共同决定。

因此不能写出这样的业务假设:

因为 reader1 先 pthread_create,所以它一定先于 writer2 读取

如果业务需要固定顺序,应使用条件变量、信号量、队列或明确的join关系,不能依赖创建顺序和sleep()。

11.sleep()在录音示例中的作用

录音使用sleep()有两个教学目的:

  1. 放大未加锁写操作的交错,更容易观察丢失更新;
  2. 延长持有读锁或写锁的时间,更容易看清其他线程是否被阻塞。

sleep()不能提供同步保证。机器负载、调度策略和运行环境改变后,线程顺序仍可能变化。

12. 什么时候读写锁比互斥锁合适

读写锁更适合:

  • 共享数据读多写少;
  • 一次读取工作并非极短;
  • 多个读者并发确实能提高吞吐量;
  • 能接受更复杂的锁规则和潜在调度问题。

普通互斥锁可能更合适:

  • 写操作频繁;
  • 临界区很短;
  • 并发读带来的收益很小;
  • 更重视代码简单和容易验证。

读写锁并不天然比互斥锁快,应通过实际负载测量。

13. 常见错误

错误后果正确做法
读者加锁,写者不加锁同步协议失效,仍有数据竞争所有读写者使用同一把锁
写线程误用rdlock多个写者可能同时进入修改共享状态必须用wrlock
读取可变数据时完全不加锁可能看到不一致状态获取读锁后再读取
忘记unlock后续线程长期阻塞每条退出路径都释放锁
未持锁却调用unlock未定义行为只释放当前线程实际持有的锁
锁仍被持有时销毁未定义行为所有线程结束并释放后销毁
用sleep规定执行顺序结果随调度变化使用正式同步机制
认为先创建必然先执行业务顺序不可靠显式表达依赖关系
在读锁中修改共享数据破坏多个读者并发的前提修改时获取写锁

14. 录音与教材表述校正

  1. 转写中的“图形锁”“图像处理”在本节多数应为“读写锁”。
  2. 读者能够并发,不表示读共享内存永远安全;如果存在写者,读者也必须参与同步。
  3. pthread_rwlock_t的内部结构属于具体实现,应用代码不应依赖其字段。
  4. 动态初始化必须传两个参数:pthread_rwlock_init(&rwlock, NULL)。
  5. 静态初始化使用PTHREAD_RWLOCK_INITIALIZER,不能再对同一对象重复初始化。
  6. 读锁和写锁都通过同一个pthread_rwlock_unlock()释放。
  7. 两个未加锁写线程得到 1 是教学中常见现象,但数据竞争属于未定义行为,不能保证每次都得到同样结果。
  8. 创建写线程后调用sleep(3)只能增加随后读到最终值的概率,不能构成严格同步。
  9. 读写锁释放后究竟由读者还是写者先获得,不能依赖普通程序中的固定猜测。

15. 本节最低掌握标准

学完后应能回答:

  1. 读锁和写锁分别允许哪些线程并发?
  2. 为什么读线程也要加锁?
  3. 为什么写线程必须使用pthread_rwlock_wrlock()?
  4. 静态初始化和动态初始化怎样写?
  5. 读锁和写锁分别怎样释放?
  6. 为什么pthread_create()的先后顺序不等于实际执行顺序?
  7. sleep()为什么不能替代同步机制?
  8. 什么情况下读写锁可能比互斥锁更合适?

最低实践要求:

  • 独立写出一个读线程和一个写线程;
  • 正确使用rdlock → 读取 → unlock;
  • 正确使用wrlock → 修改 → unlock;
  • 等待所有线程结束后再销毁读写锁;
  • 能解释未加写锁时发生的丢失更新。

16. PDF 页码与录音时间索引

主题PDF 页码录音时间
读写锁工作原理15000:05—01:00
类型、初始化与销毁15101:00—04:23
rdlock、wrlock、unlock151—15202:00—04:23
未加写锁的实验152—15404:24—11:45
给写操作添加写锁154—15611:46—13:55
读写操作顺序实验准备156—15813:56—15:02
写饥饿158 页起本次截止点之后,下一节学习

进一步核对资料:

  • POSIX Programmer’s Manual:读写锁初始化与销毁
  • POSIX Programmer’s Manual:获取读锁
  • POSIX Programmer’s Manual:获取写锁
  • POSIX Programmer’s Manual:释放读写锁

17. 下一节预习边界

第 158 页后半开始讨论写饥饿:读者持续到来时,等待中的写者可能长时间无法获得写锁。这个主题对应录音 17:56 以后,不属于本次 15:02 前的学习范围。

继续学习时重点关注:

  • 为什么已有写者等待时,新读者是否还能进入会影响公平性;
  • Linux 中读写锁属性怎样改变读写优先策略;
  • 为什么“读者并发越多越好”并不总是成立。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 22:29:56

rust redis = “0.29.1“ 有没有连接池

redis "0.29.1" 这个 crate 本身没有内置连接池。它提供的是&#xff1a;redis::Client&#xff1a;Redis 客户端client.get_connection()&#xff1a;获取单个同步连接client.get_multiplexed_async_connection()&#xff1a;获取单个异步可复用连接这些都不是连接…

作者头像 李华
网站建设 2026/10/3 22:27:10

空气幕:门店厂房必备的气流隔绝节能设备

空气幕又称风幕机&#xff0c;是广泛应用于商铺、厂房、商超、门厅的通风隔热设备&#xff0c;依靠高速气流形成均匀无形的风幕屏障&#xff0c;有效分隔室内外空气&#xff0c;适配各类常开式门口场景使用。Air curtains, also known as air door machines, are ventilation a…

作者头像 李华
网站建设 2026/10/3 22:22:53

门窗安装---什么是干法、湿法?安装位置定在哪?玻璃垫块什么用?

门窗安装---什么是干法、湿法? 安装位置定在哪?玻璃垫块什么用? 安装出问题,门窗却成了背锅侠。三分料,七分装,好手艺锦上添花,甚至还能弥补一些产品可能有的细微瑕疵,手艺不到家就会白白浪费一樘好窗,并且会导致不少隐患,这关键一环如何把握。 先说门窗自身的安装…

作者头像 李华
网站建设 2026/10/3 22:14:39

AI 工具接入 MCP 的三种传输方式:stdio、SSE 与 streamable-http 怎么选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华