当前位置: 首页 > news >正文

关于对 第 12 章 读/写者的一点思考和题解 (作业 12.19,12.20,12.21)

近刚学完 Linux 的进程部分, 接下来就是研究并发了. 正好, 去年 12 月到今年 1 月份那会, 我浅浅学了 CS:APP 的第 12 章。

但是, 当时因为我出了一点事情(主要是严重感冒+一些杂七杂八的事情), 所以没有好好研究, 粗略理解了一下概念就过去了, 习题也没好好做.

现在, 我重新复习了一下这一章, 并且重做了一下这些题目. 先从读/写者模型开始吧.

1. 什么是读/写者模型?

想象一张很大的白纸, 我们可以往白纸上写字, 擦掉字, 也可以用眼睛在白纸上看别人写的字然后记住.

我们把往白纸上写字的人叫做写者, 把记住别人所写的字的人叫做读者.

在计算机这个大系统中, 也有类似的场景. 如 CS:APP 中所讲, 内存其实就是白纸, 负责从资源中读取的进程叫做读者, 而负责往资源中写数据的进程叫做写者.

但是, 要是读者和写者不按照一定的规律来的话, 那整个系统就会乱套了. 所以, 为了保证顺序, 操作系统引入了信号量, 用来保证读写的操作是原子操作. 这部分在 CS:APP 的 12.5 节, 就不多讲了.

2. 读/写者的实现

为了实现上述的代码, 我们来写一下读/写者的实现逻辑:

sem_t w;
void reader() {
P(&w);
//Read
V(&w);
}
void writer() {
P(&w);
//Write
V(&w);
}

由此可见, 给 reader 和 writer 都加上一个w的互斥锁后, 下面的这段读/写代码就能实现原子操作了. 但是, 这样又带来了一个问题--太慢!

你想啊, 这样子会导致读写操作完全串行, 等于把多任务变成了单任务了, 这已经不是并发了. 这怎么行?

那你可能会说, 那我把读者的锁去掉, 行不行? 反正读者不改数据. 那接下来看一个例子---

typedef struct {
int a;
int b;
} obj_t;
static volatile obj_t o;
/**
* 假设 o = {.a = 1, .b = 2};
*/
sem_t w;
void reader() {
obj_t o2 = o;
/**
* 这段代码反汇编后重新翻译, 大概是这样的:
* o2.a = o.a;
* o2.b = o.b;
*/
//Operate
}
void writer(obj_t o2) {
P(&w);
o = o2; //同上
V(&w);
}

那我们来验证一下, 假设writer函数写入一个数据如下:

obj_t o3 = {
.a = 3;
.b = 4;
};

有如下表格:

时刻谁在执行?执行的指令数据
1writerP(&mutex)-
2readero2.a = o.ao2.a = 1
3writero.a = o2.ao.a = 3
4writero.b = o2.bo.b = 4
5readero2.b = o.bo2.b = 4
6writerV(&mutex)-

最终 reader 读取到的结果:

o2 = {
.a = 1;
.b = 4;
}

这缝合怪可不是我们想要的啊! 也就是说, 这种方案, 可能会导致数据的残缺! 那怎么办? CS:APP 给我们提供了一段既可以加速, 又保证数据安全的示例代码 ---

3. 对于图 12.26 代码的一点研究

先摆上这段代码, 它位于 CSAPP 的图 12.26:

int readcnt; // 刚开始=0
sem_t mutex, w; // 刚开始全部都为1
void reader(void) {
while(1) {
P(&mutex);
readcnt++;
if(readcnt == 1) {
P(&w);
}
V(&mutex);
// 读取操作
P(&mutex);
readcnt--;
if(readcnt == 0) {
V(&w);
}
V(&mutex);
}
}
void writer(void) {
while(1) {
P(&w);
// 写入操作
V(&w);
}
}

很明显, 这段代码相对于上面的示例代码, 有了如下改动:

  • 读写者都有一个互斥锁w保护着, 保证数据在写的同时不会被读取, 数据在读取的时候不会被写入.
  • 增加了一个变量readcnt, 用来记录此时此刻读者的数量.
  • 为了保证变量readcnt原子性, 让它不受并发的影响, 新增了一个mutex互斥锁用来保护它.
  • readcnt == 1的时候, 代表有读者在读了, 第一个读者获取了w.
  • 一旦有一个读者获取了锁, 其他读者都可以直接读数据, 不用再受w的约束了, 也就是说, 它真正实现了并发.
  • 当写者获取锁之后, 由于第一个读者卡在了锁&w的获取, 其他多余的读者都卡在了mutex的锁的获取, 所以此时读者不能读取数据.

这相当于是上述两个方案的结合体, 也就是说, 它既能保证读者的并发, 也能保证数据的安全.

但是, 随之而来的又有一些问题:

  • 假设写者来的时候, 读者正在读取数据, 然后读取数据的时候又有读者来,readcnt++, 这样下去的话, 写者就没机会执行了, 这咋整?
  • 这种方案, 虽然读者的优先级比写者高, 但是其实这种优先级很弱, 因为写者执行了 V(&w) 后, 可能立马又绕回到while开头了, 从而导致

但是, 很明显, 这段代码也有一点缺陷:

  • 这段代码中, 读者的优先级比写者高, 因为就算写者先来, 只要读者在读数据, 写者永远都没机会执行, 导致写者饥饿.
  • (习题 12.10)这段代码中, 读者的优先级虽然比写者高, 但也只是高一点. 对于锁w的操作, 读者和写者优先级相同, 万一有一堆写者, 读者很少, 那么读者就不能保证这个优先级了, 写者执行完V(&w)后, 下一个P(&w)照样大概率是写者, 导致读者饥饿.

那么, 接下来我们要做一些改动, 就是下面的几道题目:

4. 课后作业的题解

4.1 作业 12.19

这道题可以看作是 习题12.10 的延续:

翻译成人话就是:写者执行完V(&w)后, 由读者来执行下一个P(&w).

这道题我想了好久, 有好多方案都依赖操作系统的调度器(例如 CFS 就能保证先睡醒的进程先执行)或者额外的 API(例如sched_yield), 或者writer部分就只能充当自旋锁(也就是dreamanddead的版本, 这个reader_first == 1条件过后, 不停的continue当自旋锁, 这太耗费 CPU 了).

后面参考了知乎用户Decay的一个专栏, 秒懂:

//省略 while(1), 下同
void writer() {
//前面省略
V(&w);
P(&mutex);
V(&mutex);
}

逻辑也比较简单:

  • 如果此时此刻有读者在等待w, 那么mutex锁肯定是被锁住的
  • 所以writer只需要等待mutex, 也就是等待读者释放mutex锁, 而读者要是能释放mutex锁, 那肯定就已经获取w了, 也就是说, 这个是能保证读者强优先的.
  • 如果没读者锁住mutex, 那么就自己锁住自己解锁.

问题得以解决.

4.2 作业 12.20

ok, 解决了 12.19 后, 接下来就是 12.20.

翻译成人话, 就是:先到先得,读者来就先执行读者,写者来就先执行写者。但是这并不等同于串行, 例如这种情况:

读者 A, 读者 B, 写者 C, 读者 D
读者 A 先读取, 读者 B 接下来发现目前状态是在读, 也进去读.
但是写者 C 就直接停住了, 因为当前状态是在读, 而它要进行写操作.
由于先到先得, 读者 D 也停住了, 等待 C 执行完.

题目给了我们提示:最多 N 个读者, 一个计数信号量, 一个互斥锁.

ok, 现在开始写代码.

sem_t num; //初始化为 N
sem_t mutex;
int write = 0;
void reader() {
P(&mutex);
P(&num); //read_num -= 1;
V(&mutex);
//Read
V(&num);
}
void writer() {
P(&mutex);
//Write
V(&mutex);
}

我认为是这样的逻辑:

  • num就是读者剩下的读写者, 题目中说最多 N 个, 所以初始化为N.
  • 每个读者进来一次, 就P(&num).
  • mutexreaderwriter的竞争锁. 在前一个reader来了后, 分为两种情况:
    • writer来了,writer要等写入完后, 才能执行reader, 所以接下来的reader全部阻塞在mutex上, 就没法执行了.
    • reader来了, 由于reader在将剩下的读写者-1后, 立马释放了锁, 所以reader还是可以继续并发执行的.

OK, 问题解决.

4.3 作业 12.21

继续看下一道题目:

这难度看样子不小, 其实就根据 图12.26+作业 12.10 照葫芦画瓢就好了. 话不多说开始写代码:

sem_t mutex;
sem_t w;
void reader() {
P(&w);
//Read
V(&w);
P(&mutex);
V(&mutex);
}
void writer() {
P(&mutex);
P(&w);
V(&mutex);
//Write
V(&w);
}

这儿确实和上面的有些不一样, 我没有设置writecnt, 因为writerreader不一样,writer不可能并发运行, 否则乱套了.

而上面的mutex也是用于等待writer的, 逻辑和上面的一样, 要是writer在等待,mutex肯定是锁住的.

http://www.jsqmd.com/news/587506/

相关文章:

  • 网红旺仔蓝牙音响详细教程 | 制作成本不到50!
  • Qwen3.6-Plus 技术深度拆解:500K 超长上下文与 MoE 架构的再进化
  • 炉石传说脚本终极指南:3小时变8分钟的智能游戏体验
  • Ollama-for-amd全攻略:AMD GPU本地AI部署革新性实践指南
  • 激活函数选型指南:从Sigmoid到Swish,实战中如何根据任务和框架做选择?
  • Android ImageButton进阶实战:从基础到自定义状态与交互优化
  • 实战指南:基于快马AI生成简易CPU模拟器,深入理解指令执行全流程
  • 为什么门禁时灵时不灵?你可能忽略了识别距离
  • GetQzonehistory:永久保存QQ空间青春回忆的智能备份工具
  • 如何用douyin-downloader实现抖音视频批量下载?5个技巧让内容管理效率提升10倍
  • 智能家居报警系统避坑指南:从MQ-2烟雾传感器到HC-SR501人体感应,这些细节决定成败
  • 5分钟搞定GB/T 7714参考文献格式:中国学者的终极解决方案
  • C语言联合体(共用体)的妙用:从判断大小端到节省内存的嵌入式开发技巧
  • 第 5 章 触觉与力觉感知
  • HTTPS证书如何申请?:从入门到精通,守护网站安全
  • DreamZero技术解析:当视频扩散模型成为机器人“物理大脑“
  • Graphormer模型解释性研究:可视化注意力机制揭示分子关键子结构
  • 用开源模拟器重构经典游戏体验:FinalBurn Neo的跨时代技术实践
  • 告别Keil和IAR?试试这款专为RISC-V打造的免费IDE:MounRiver Studio深度体验
  • 快速搭建小龙虾openclaw机器人控制原型:快马平台助力机械臂算法验证
  • intv_ai_mk11效果惊艳:技术概念解释附带类比(如‘注意力机制像老师点名’)提升理解
  • Python实战:基于余弦相似度的中文短文本相似性计算
  • c++编程:科学计数法(1024-PAT乙级)
  • 华硕笔记本性能优化新选择:GHelper高效硬件控制工具深度解析
  • 阿里通义Z-Image-GGUF功能体验:中英文提示词支持实测
  • 小米智能家居与Home Assistant零门槛实战:从集成到优化全流程指南
  • 如何为你的外贸网站选择最佳网络线路:CN2 vs BGP vs 3C vs 阿里云
  • 利用快马平台与accelerate库,十分钟搭建你的第一个分布式训练原型
  • 从Dirty COW到内核攻防:竞态条件漏洞的现代利用与防御思考
  • 告别Fiddler和Charles,用Proxyman在Android 13上抓HTTPS包(附network_security_config.xml配置)