Linux 线程同步:读写锁

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() 有两个教学目的:

  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 页码 录音时间
读写锁工作原理 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 页起 本次截止点之后,下一节学习

进一步核对资料:

17. 下一节预习边界

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

继续学习时重点关注:

  • 为什么已有写者等待时,新读者是否还能进入会影响公平性;
  • Linux 中读写锁属性怎样改变读写优先策略;
  • 为什么"读者并发越多越好"并不总是成立。
相关推荐
蜗牛互联网1 小时前
从Barclays扩大Claude部署看受监管软件工程的责任重分配
java·大数据·人工智能·后端·软件工程
天远API1 小时前
零信任架构实战:基于天远股权穿透构建自动化供应链授信穿透网关
java·人工智能·架构·自动化
工作10年+,存储芯片行业1 小时前
Linux NVMe 中断排查与性能优化:IRQ 优化(原理+排查)
linux·运维·性能优化·ssd·nvme·pcie·中断
最强小杰1 小时前
claude-opus-5.5 API 接入教程:anthropic-version 头、tools 定义、流式事件格式三个常见问题全部梳理,收藏备用
java·服务器·网络·ai
Cc.Y1 小时前
Java零基础入门:方法(函数)深度掌握
java·开发语言
宵时待雨2 小时前
linux笔记归纳25:多路转接epoll
linux·服务器·网络·c++
奋发向前wcx2 小时前
CSP-J复赛模拟赛1 王晨旭补题 2026.10.2
java·开发语言
卓怡学长2 小时前
w192基于springboot“考研情报站”微信小程序设计与实现
java·spring boot·spring·微信小程序·intellij-idea
geats人山人海2 小时前
linux 1.目录结构
linux·运维·服务器