Linux 线程同步:读写锁
课程:尚硅谷《嵌入式 Linux 应用层开发》第 4 章线程处理
依据:2026-09-29 19:30 录音转写,整理到 15:02 ;课程 PDF 第 150---158 页。
本节边界:讲清读写锁原理、基础 API、未加写锁的错误示例、加写锁后的修正,以及读写线程执行顺序实验的准备。第 158 页开始的"写饥饿"对应录音 17:56 以后,留到下一节。
1. 为什么互斥锁之后还要学习读写锁
互斥锁不区分读和写。只要一个线程持有互斥锁,其他线程都要等待。
但很多共享数据具有"读得多、写得少"的特点:
- 多个线程只读取同一份稳定数据时,可以并发执行;
- 写线程修改数据时,必须独占;
- 写入期间不能让读线程看到一半旧、一半新的状态。
读写锁(read-write lock)用两种加锁方式表达这种规则:
text
读锁:共享占用,允许多个读者并发
写锁:独占占用,只允许一个写者进入
2. 读锁与写锁的兼容关系
| 当前状态 | 新读者申请读锁 | 新写者申请写锁 |
|---|---|---|
| 没有线程持锁 | 可以获得 | 可以获得 |
| 一个或多个线程持有读锁 | 通常可继续获得读锁 | 必须等待全部读锁释放 |
| 一个线程持有写锁 | 必须等待 | 必须等待 |
"通常可继续获得读锁"还受调度策略以及是否已有写者等待等因素影响,不能据此假定读者永远优先。
#mermaid-svg-9ZOASPgvVT1E1YEj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9ZOASPgvVT1E1YEj .error-icon{fill:#552222;}#mermaid-svg-9ZOASPgvVT1E1YEj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9ZOASPgvVT1E1YEj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9ZOASPgvVT1E1YEj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9ZOASPgvVT1E1YEj .marker.cross{stroke:#333333;}#mermaid-svg-9ZOASPgvVT1E1YEj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9ZOASPgvVT1E1YEj p{margin:0;}#mermaid-svg-9ZOASPgvVT1E1YEj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj .cluster-label text{fill:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj .cluster-label span{color:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj .cluster-label span p{background-color:transparent;}#mermaid-svg-9ZOASPgvVT1E1YEj .label text,#mermaid-svg-9ZOASPgvVT1E1YEj span{fill:#333;color:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj .node rect,#mermaid-svg-9ZOASPgvVT1E1YEj .node circle,#mermaid-svg-9ZOASPgvVT1E1YEj .node ellipse,#mermaid-svg-9ZOASPgvVT1E1YEj .node polygon,#mermaid-svg-9ZOASPgvVT1E1YEj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9ZOASPgvVT1E1YEj .rough-node .label text,#mermaid-svg-9ZOASPgvVT1E1YEj .node .label text,#mermaid-svg-9ZOASPgvVT1E1YEj .image-shape .label,#mermaid-svg-9ZOASPgvVT1E1YEj .icon-shape .label{text-anchor:middle;}#mermaid-svg-9ZOASPgvVT1E1YEj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9ZOASPgvVT1E1YEj .rough-node .label,#mermaid-svg-9ZOASPgvVT1E1YEj .node .label,#mermaid-svg-9ZOASPgvVT1E1YEj .image-shape .label,#mermaid-svg-9ZOASPgvVT1E1YEj .icon-shape .label{text-align:center;}#mermaid-svg-9ZOASPgvVT1E1YEj .node.clickable{cursor:pointer;}#mermaid-svg-9ZOASPgvVT1E1YEj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9ZOASPgvVT1E1YEj .arrowheadPath{fill:#333333;}#mermaid-svg-9ZOASPgvVT1E1YEj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9ZOASPgvVT1E1YEj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9ZOASPgvVT1E1YEj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9ZOASPgvVT1E1YEj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9ZOASPgvVT1E1YEj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9ZOASPgvVT1E1YEj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9ZOASPgvVT1E1YEj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9ZOASPgvVT1E1YEj .cluster text{fill:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj .cluster span{color:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9ZOASPgvVT1E1YEj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9ZOASPgvVT1E1YEj rect.text{fill:none;stroke-width:0;}#mermaid-svg-9ZOASPgvVT1E1YEj .icon-shape,#mermaid-svg-9ZOASPgvVT1E1YEj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9ZOASPgvVT1E1YEj .icon-shape p,#mermaid-svg-9ZOASPgvVT1E1YEj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9ZOASPgvVT1E1YEj .icon-shape .label rect,#mermaid-svg-9ZOASPgvVT1E1YEj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9ZOASPgvVT1E1YEj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9ZOASPgvVT1E1YEj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9ZOASPgvVT1E1YEj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 读
否
是
写
否
是
线程申请访问共享数据
读还是写
当前是否有写者持锁
取得读锁
阻塞等待
读取共享数据
释放读锁
是否有任何读者或写者持锁
取得写锁
修改共享数据
释放写锁
3. "多个线程同时读没问题"的适用条件
录音用"大家一起看已经写好的黑板"解释读锁。这个比喻成立需要满足:
- 读者只读取,不修改;
- 对象在读取期间仍然存在;
- 没有写者绕过锁修改;
- 所有读者和写者使用同一把读写锁。
如果共享数据可能被写线程修改,读线程也必须获取读锁。否则读线程可能与写线程形成数据竞争,或看到不一致的组合状态。
4. pthread_rwlock_t 的使用原则
读写锁类型声明为:
c
#include <pthread.h>
pthread_rwlock_t rwlock;
教材第 151 页展示了 glibc 内部的联合体定义。应用程序应把 pthread_rwlock_t 当作不透明类型:
- 只通过
<pthread.h>使用; - 不访问内部成员;
- 不复制读写锁对象;
- 给相关函数传入同一个对象的地址。
5. 初始化与销毁
5.1 静态初始化
默认属性下,可以在定义时初始化:
c
static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
这种方式不再调用 pthread_rwlock_init()。
5.2 动态初始化
c
pthread_rwlock_t rwlock;
int error = pthread_rwlock_init(&rwlock, NULL);
第二个参数是属性对象。传 NULL 表示使用默认属性。
5.3 销毁
c
int error = pthread_rwlock_destroy(&rwlock);
销毁前必须确认:
- 所有使用它的线程已经结束或不再访问它;
- 当前没有读者或写者持锁;
- 不会再有线程等待这把锁。
静态初始化只表示不需要调用 pthread_rwlock_init()。如果锁与进程同寿命,程序退出时系统会回收进程资源;如果对象有独立、可重复的生命周期,则应在安全时显式销毁。
6. 本节五个核心 API
c
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
| 函数 | 作用 | 获取不到时 |
|---|---|---|
pthread_rwlock_rdlock |
获取读锁 | 有写者阻挡时等待 |
pthread_rwlock_wrlock |
获取写锁 | 只要还有读者或写者,就等待 |
pthread_rwlock_unlock |
释放当前线程持有的一次读锁或写锁 | 不适用 |
同一把读写锁的读锁和写锁都用 pthread_rwlock_unlock() 释放。
Pthreads 函数成功返回 0,否则返回错误号。基础检查写法:
c
int error = pthread_rwlock_rdlock(&rwlock);
if (error != 0) {
fprintf(stderr, "pthread_rwlock_rdlock: %s\n",
strerror(error));
}
7. 第一个实验:写操作没有加锁
教材第 152---154 页故意让两个写线程修改同一个变量,却不给写操作加锁:
c
static int shared_data = 0;
static void *writer_without_lock(void *arg)
{
int temp = shared_data + 1;
sleep(1);
shared_data = temp;
printf("%s 完成写入,shared_data = %d\n",
(char *)arg, shared_data);
return NULL;
}
sleep(1) 是为了放大两个写线程发生交错的机会:
| 步骤 | 写线程 1 | 写线程 2 | shared_data |
|---|---|---|---|
| 1 | 读取 0,算出 temp=1 |
0 | |
| 2 | 睡眠 | 读取 0,算出 temp=1 |
0 |
| 3 | 睡眠 | 0 | |
| 4 | 写回 1 | 1 | |
| 5 | 写回 1 | 1 |
预期加两次得到 2,实际可能得到 1。这与上一节 number++ 的丢失更新本质相同。
7.1 只有读线程加读锁,为什么仍然错误
教材错误示例中的读线程这样写:
c
pthread_rwlock_rdlock(&rwlock);
printf("shared_data = %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
读线程之间可以互相协调,但两个写线程完全没有使用这把锁。读写锁只有在所有访问者共同遵守时才有效。
text
读者使用 rwlock + 写者绕过 rwlock
↓
同步协议不完整
8. 修正实验:写线程获取写锁
教材第 154---156 页给写操作补上写锁:
c
static void *lock_writer(void *arg)
{
pthread_rwlock_wrlock(&rwlock);
int temp = shared_data + 1;
sleep(1); /* 仅为演示持锁期间其他线程会等待 */
shared_data = temp;
printf("%s 完成写入,shared_data = %d\n",
(char *)arg, shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
执行过程变成:
写线程 2 读写锁 写线程 1 写线程 2 读写锁 写线程 1 #mermaid-svg-4fgLNwlvtZDIHrFr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4fgLNwlvtZDIHrFr .error-icon{fill:#552222;}#mermaid-svg-4fgLNwlvtZDIHrFr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4fgLNwlvtZDIHrFr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4fgLNwlvtZDIHrFr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4fgLNwlvtZDIHrFr .marker.cross{stroke:#333333;}#mermaid-svg-4fgLNwlvtZDIHrFr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4fgLNwlvtZDIHrFr p{margin:0;}#mermaid-svg-4fgLNwlvtZDIHrFr .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4fgLNwlvtZDIHrFr text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4fgLNwlvtZDIHrFr .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4fgLNwlvtZDIHrFr .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4fgLNwlvtZDIHrFr #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4fgLNwlvtZDIHrFr .sequenceNumber{fill:white;}#mermaid-svg-4fgLNwlvtZDIHrFr #sequencenumber{fill:#333;}#mermaid-svg-4fgLNwlvtZDIHrFr #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4fgLNwlvtZDIHrFr .messageText{fill:#333;stroke:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4fgLNwlvtZDIHrFr .labelText,#mermaid-svg-4fgLNwlvtZDIHrFr .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .loopText,#mermaid-svg-4fgLNwlvtZDIHrFr .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4fgLNwlvtZDIHrFr .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4fgLNwlvtZDIHrFr .noteText,#mermaid-svg-4fgLNwlvtZDIHrFr .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4fgLNwlvtZDIHrFr .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4fgLNwlvtZDIHrFr .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4fgLNwlvtZDIHrFr .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4fgLNwlvtZDIHrFr .actorPopupMenu{position:absolute;}#mermaid-svg-4fgLNwlvtZDIHrFr .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4fgLNwlvtZDIHrFr .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4fgLNwlvtZDIHrFr .actor-man circle,#mermaid-svg-4fgLNwlvtZDIHrFr line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4fgLNwlvtZDIHrFr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阻塞等待 wrlock 成功 wrlock shared_data 由 0 变 1 unlock 获得写锁 shared_data 由 1 变 2 unlock
两个写线程不再同时进入临界区,最终值稳定为 2。
9. 基础完整练习:两个写者和三个读者
下面的例子保留录音中的主要现象,同时补上返回值检查。读者看到 0、1 或 2 都有可能,因为调度顺序不确定;但每次读取都发生在读锁保护下,最终值应为 2。
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int shared_data = 0;
static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
static void check_pthread(int error, const char *operation)
{
if (error != 0) {
fprintf(stderr, "%s: %s\n", operation, strerror(error));
exit(EXIT_FAILURE);
}
}
static void *reader(void *arg)
{
const char *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");
return NULL;
}
static void *writer(void *arg)
{
const char *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");
return NULL;
}
int main(void)
{
pthread_t threads[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 (int i = 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");
return EXIT_SUCCESS;
}
编译运行:
bash
gcc -Wall -Wextra -pthread rwlock_basic.c -o rwlock_basic
./rwlock_basic
9.1 观察重点
多运行几次,输出顺序可能改变:
- 先创建的线程不保证先执行;
- 多个读者可以同时持有读锁;
- 写者必须等所有当前读者释放;
- 写者持锁时,新的读者和其他写者都不能进入;
- 所有线程结束后,最终值为 2。
10. 为什么创建顺序不等于执行顺序
教材第 156---158 页把第二个写线程放到多个读线程之间创建,用来观察读写顺序:
text
创建 writer1
创建 reader1、reader2、reader3
创建 writer2
创建 reader4、reader5、reader6
这只是向系统提交线程的先后顺序。线程何时真正获得 CPU、何时开始申请锁,由调度器和当时的锁状态共同决定。
因此不能写出这样的业务假设:
text
因为 reader1 先 pthread_create,所以它一定先于 writer2 读取
如果业务需要固定顺序,应使用条件变量、信号量、队列或明确的 join 关系,不能依赖创建顺序和 sleep()。
11. sleep() 在录音示例中的作用
录音使用 sleep() 有两个教学目的:
- 放大未加锁写操作的交错,更容易观察丢失更新;
- 延长持有读锁或写锁的时间,更容易看清其他线程是否被阻塞。
sleep() 不能提供同步保证。机器负载、调度策略和运行环境改变后,线程顺序仍可能变化。
12. 什么时候读写锁比互斥锁合适
读写锁更适合:
- 共享数据读多写少;
- 一次读取工作并非极短;
- 多个读者并发确实能提高吞吐量;
- 能接受更复杂的锁规则和潜在调度问题。
普通互斥锁可能更合适:
- 写操作频繁;
- 临界区很短;
- 并发读带来的收益很小;
- 更重视代码简单和容易验证。
读写锁并不天然比互斥锁快,应通过实际负载测量。
13. 常见错误
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 读者加锁,写者不加锁 | 同步协议失效,仍有数据竞争 | 所有读写者使用同一把锁 |
写线程误用 rdlock |
多个写者可能同时进入 | 修改共享状态必须用 wrlock |
| 读取可变数据时完全不加锁 | 可能看到不一致状态 | 获取读锁后再读取 |
忘记 unlock |
后续线程长期阻塞 | 每条退出路径都释放锁 |
未持锁却调用 unlock |
未定义行为 | 只释放当前线程实际持有的锁 |
| 锁仍被持有时销毁 | 未定义行为 | 所有线程结束并释放后销毁 |
用 sleep 规定执行顺序 |
结果随调度变化 | 使用正式同步机制 |
| 认为先创建必然先执行 | 业务顺序不可靠 | 显式表达依赖关系 |
| 在读锁中修改共享数据 | 破坏多个读者并发的前提 | 修改时获取写锁 |
14. 录音与教材表述校正
- 转写中的"图形锁""图像处理"在本节多数应为"读写锁"。
- 读者能够并发,不表示读共享内存永远安全;如果存在写者,读者也必须参与同步。
pthread_rwlock_t的内部结构属于具体实现,应用代码不应依赖其字段。- 动态初始化必须传两个参数:
pthread_rwlock_init(&rwlock, NULL)。 - 静态初始化使用
PTHREAD_RWLOCK_INITIALIZER,不能再对同一对象重复初始化。 - 读锁和写锁都通过同一个
pthread_rwlock_unlock()释放。 - 两个未加锁写线程得到 1 是教学中常见现象,但数据竞争属于未定义行为,不能保证每次都得到同样结果。
- 创建写线程后调用
sleep(3)只能增加随后读到最终值的概率,不能构成严格同步。 - 读写锁释放后究竟由读者还是写者先获得,不能依赖普通程序中的固定猜测。
15. 本节最低掌握标准
学完后应能回答:
- 读锁和写锁分别允许哪些线程并发?
- 为什么读线程也要加锁?
- 为什么写线程必须使用
pthread_rwlock_wrlock()? - 静态初始化和动态初始化怎样写?
- 读锁和写锁分别怎样释放?
- 为什么
pthread_create()的先后顺序不等于实际执行顺序? sleep()为什么不能替代同步机制?- 什么情况下读写锁可能比互斥锁更合适?
最低实践要求:
- 独立写出一个读线程和一个写线程;
- 正确使用
rdlock → 读取 → unlock; - 正确使用
wrlock → 修改 → unlock; - 等待所有线程结束后再销毁读写锁;
- 能解释未加写锁时发生的丢失更新。
16. PDF 页码与录音时间索引
| 主题 | PDF 页码 | 录音时间 |
|---|---|---|
| 读写锁工作原理 | 150 | 00:05---01:00 |
| 类型、初始化与销毁 | 151 | 01:00---04:23 |
rdlock、wrlock、unlock |
151---152 | 02:00---04:23 |
| 未加写锁的实验 | 152---154 | 04:24---11:45 |
| 给写操作添加写锁 | 154---156 | 11:46---13:55 |
| 读写操作顺序实验准备 | 156---158 | 13: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 中读写锁属性怎样改变读写优先策略;
- 为什么"读者并发越多越好"并不总是成立。