Appearance
题目
某进程中有 3 个并发执行的线程 thread1、thread2、thread3,其伪代码如下:
c
// 复数的结构类型定义
typedef struct {
float a;
float b;
} cnum;
// 全局变量
cnum x, y, z;
// 计算两个复数之和
cnum add(cnum p, cnum q) {
cnum s;
s.a = p.a + q.a;
s.b = p.b + q.b;
return s;
}
thread1 {
cnum w;
w = add(x, y);
...
}
thread2 {
cnum w;
w = add(y, z);
...
}
thread3 {
cnum w;
w.a = 1;
w.b = 2;
z = add(z, w);
y = add(y, w);
...
}请添加必要的信号量和 P、V(或 wait、signal)操作,要求确保线程互斥访问临界资源,并且最大程度地并发执行。
解析
关系拆解
第一步:列出每个线程对每个变量的读 / 写动作
| 线程 \ 变量 | x | y | z |
|---|---|---|---|
| thread1 | 读 | 读 | — |
| thread2 | — | 读 | 读 |
| thread3 | — | 写 | 写 |
第二步:判定每对线程在每个变量上的关系
关键规则:
- 读读 = 不冲突(多线程同时读没问题)
- 读写 / 写写 = 冲突(必须互斥)
| 变量 | thread1 ↔ thread2 | thread1 ↔ thread3 | thread2 ↔ thread3 |
|---|---|---|---|
| x | 不共享(只 t1 读) | 不共享 | 不共享 |
| y | 同时读(无冲突) | 读写冲突 ✗ | 读写冲突 ✗ |
| z | 不共享 | 不共享 | 读写冲突 ✗ |
第三步:从冲突推导信号量
3 处冲突,但不能简单地用 3 个信号量——还要考虑同一个线程同时持多锁会不会死锁:
- thread1 ↔ thread3 关于 y → 信号量
y_mutex1 - thread2 ↔ thread3 关于 y → 信号量
y_mutex2 - thread2 ↔ thread3 关于 z → 信号量
z_mutex
为什么 y 要分两把锁?因为 thread1 只跟 thread3 关于 y 冲突,thread2 也只跟 thread3 关于 y 冲突——但 thread1 和 thread2 之间在 y 上无冲突(都是读)。如果 thread1、thread2 共用一把 y_mutex,就会人为串行化它们的"读 y"操作,降低并发度。所以拆成两把,让 thread1 和 thread2 真正并行读 y。
信号量与代码
c
semaphore y_mutex1 = 1; // 调节 thread1 ↔ thread3 关于 y 的读写冲突
semaphore y_mutex2 = 1; // 调节 thread2 ↔ thread3 关于 y 的读写冲突
semaphore z_mutex = 1; // 调节 thread2 ↔ thread3 关于 z 的读写冲突
thread1() {
cnum w;
P(y_mutex1); // 锁定 y(防 thread3 写 y)
w = add(x, y); // 读 x(无冲突)+ 读 y
V(y_mutex1);
// ...
}
thread2() {
cnum w;
P(z_mutex); // 锁定 z(防 thread3 写 z)
P(y_mutex2); // 锁定 y(防 thread3 写 y)
w = add(y, z); // 读 y、读 z
V(y_mutex2);
V(z_mutex);
// ...
}
thread3() {
cnum w;
w.a = 1;
w.b = 2;
// 写 z:跟 thread2 冲突
P(z_mutex);
z = add(z, w);
V(z_mutex);
// 写 y:跟 thread1、thread2 都冲突
P(y_mutex1);
P(y_mutex2);
y = add(y, w);
V(y_mutex2);
V(y_mutex1);
// ...
}信号量含义说明(答题必写)
| 信号量 | 初值 | 调节谁与谁、关于谁 |
|---|---|---|
y_mutex1 | 1 | thread1 ↔ thread3 关于 y |
y_mutex2 | 1 | thread2 ↔ thread3 关于 y |
z_mutex | 1 | thread2 ↔ thread3 关于 z |
易错点
编者注(设计精妙处):
为什么 y 用 2 把锁而不是 1 把?
如果只用一个
y_mutex,那么 thread1 和 thread2 都要 P(y_mutex) 才能读 y——但它俩本质上只是两个读者,不会冲突。让它们互斥就会无故串行化两个本可以并发的读。评分细则里专门提到:"考生仅使用一个互斥信号量,互斥代码部分的得分最多给 2 分"——大约扣 1 分。这就是惩罚"未做到最大并发"。
thread2 / thread3 中"P(z_mutex) 在 P(y_mutex) 之前"必须一致
死锁规避的核心:所有线程对多把锁的获取顺序必须一致。如果一个线程先 P(z) 后 P(y)、另一个先 P(y) 后 P(z),就会构成"环形等待" → 死锁。
本题里 thread2 和 thread3 都按 z → y 顺序加锁(thread3 写 y 时按 y_mutex1 → y_mutex2 顺序,thread2 也要按相同顺序加)。约定一个统一顺序就安全。
thread3 中先写 z 再写 y(按代码顺序)
thread3 的 z = add(z, w) 必须先做(题面要求"先 z 后 y"按代码序)。所以 z 的临界区在 y 的临界区之外。两个临界区串联,不嵌套——简洁、不易死锁。
不要给 x 加信号量——x 只被 thread1 读,没有任何冲突。加锁是冗余、扣分。
w 是局部变量,不需要保护——三个线程各自的 w 互不可见,纯局部变量没有共享问题。
return 语句不影响临界区分析——
s = add(p, q)中的 s 是 add 函数的局部变量,跟全局变量 x/y/z 无关,纯函数计算无副作用。总结:本题考的不是"会用信号量",而是"精准识别冲突、最小化锁数、最大化并发"。这是工业级并发编程的核心能力。