Skip to content

2017年 408 操作系统 第 46 题

操作系统2017年综合题8分

题目

某进程中有 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)操作,要求确保线程互斥访问临界资源,并且最大程度地并发执行。

解析

关系拆解

第一步:列出每个线程对每个变量的读 / 写动作

线程 \ 变量xyz
thread1
thread2
thread3

第二步:判定每对线程在每个变量上的关系

关键规则

  • 读读 = 不冲突(多线程同时读没问题)
  • 读写 / 写写 = 冲突(必须互斥)
变量thread1 ↔ thread2thread1 ↔ thread3thread2 ↔ 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_mutex11thread1 ↔ thread3 关于 y
y_mutex21thread2 ↔ thread3 关于 y
z_mutex1thread2 ↔ thread3 关于 z

易错点

编者注(设计精妙处)

  1. 为什么 y 用 2 把锁而不是 1 把?

    如果只用一个 y_mutex,那么 thread1 和 thread2 都要 P(y_mutex) 才能读 y——但它俩本质上只是两个读者,不会冲突。让它们互斥就会无故串行化两个本可以并发的读

    评分细则里专门提到:"考生仅使用一个互斥信号量,互斥代码部分的得分最多给 2 分"——大约扣 1 分。这就是惩罚"未做到最大并发"。

  2. 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 也要按相同顺序加)。约定一个统一顺序就安全。

  3. thread3 中先写 z 再写 y(按代码顺序)

    thread3 的 z = add(z, w) 必须先做(题面要求"先 z 后 y"按代码序)。所以 z 的临界区在 y 的临界区之外。两个临界区串联,不嵌套——简洁、不易死锁。

  4. 不要给 x 加信号量——x 只被 thread1 读,没有任何冲突。加锁是冗余、扣分。

  5. w 是局部变量,不需要保护——三个线程各自的 w 互不可见,纯局部变量没有共享问题。

  6. return 语句不影响临界区分析——s = add(p, q) 中的 s 是 add 函数的局部变量,跟全局变量 x/y/z 无关,纯函数计算无副作用。

总结:本题考的不是"会用信号量",而是"精准识别冲突、最小化锁数、最大化并发"。这是工业级并发编程的核心能力。

最后更新:

⚠️ 这道题暂未配可视化,欢迎在 CodeBrick 反馈区告诉我们你想看哪道题