Vince Jiang

CSCI3150 / 辅导 · 本站更新

CSCI3150 T05 条件变量与屏障

课程:CSCI3150 Introduction to Operating Systems · 2025T2 本讲:T05 条件变量与屏障 · 原件 T05-条件变量-2025T2.pptx(本地课件) · PDF页码副本(本地课件) p.1–20 定位:把等待谓词写清,再看多生产者、屏障与哲学家练习的边界。 前后:前接 T04;后接 T06。

逐页伴读

p.1–3:历史作业提醒

p.1 Condition variables via pthread Library in C

CSCI3150 Introduction to Operating Systems, Spring 2026

p.2 Assignment 2

Invitation URL: https://classroom.github.com/a/RKvPmvxf

Deadline: 23:59:59 p.m., Sun, Mar 1st, 2026

Submission: Github classroom (refer to the Assignment 1 guide: https://piazza.com/class/mjxsr09gf91dw/post/11)

p.3 Assignment 2

For students who have already accepted asg2, please run git pull to update

封面2026年2月4日,p.2–3的3月1日作业截止与git pull提示属于该学期。本文不执行更新命令,也不更改作答。

p.4–6:从mutex转到“何时才能继续”

p.4 All examples used in this tutorial:

https://github.com/henryhxu/CSCI3150/tree/2026-Spring/tutorial

p.5 The Pthread (POSIX threads) library is a powerful tool for creating and managing threads in C. It provides a standardized API to facilitate multi-threading, which is essential for developing concurrent applications.

课件程序与接口(p.5,原页片段)

To use it:  #include <pthread.h>
Key Interfaces:
pthread_create
pthread_exit
pthread_join

mutex解决谁可同时操作,CV解决条件未满足时怎样睡眠。课程列init、wait、signal、broadcast、destroy,使用时还需要共享谓词和保护它的mutex,不能只有CV变量。

p.7–9:wait内部示意不能当普通函数复刻

课件程序与接口(p.7,原页片段)

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

p.7 Parameters:

cond: pointer to the condition variable.

attr: optional attributes for the condition variable (use NULL for default).

课件程序与接口(p.7,原页片段)

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

课件程序与接口(p.7,原页片段)

pthread_cond_t cond;
pthread_cond_init(&cond, NULL);

p.8 Purpose: wait for a condition variable to be signaled, releasing the associated mutex.

课件程序与接口(p.8,原页片段)

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

p.8 Parameters:

cond: pointer to the condition variable.

mutex: pointer to an associated mutex (must be locked before calling).

课件程序与接口(p.8,原页片段)

pthread_mutex_lock(&mutex);
while (condition_is_false) {
    pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

p.9 Question: why a mutex will be passed into this function?

课件程序与接口(p.9,原页片段)

pthread_cond_wait(pthread_cond_t* C, pthread_mutex_t* M)
{
    <put this thread into wakeup queue of condtion var C.>
    pthread_mutex_unlock(M);
    sleep();
    pthread_mutex_lock(M);
    <take this thread out of wakeup queue of condition var C.>
}

CV可静态或动态初始化。wait前持锁,在while中检查条件,wait原子释放锁并入队,醒来重新获得锁。p.9把动作拆成解锁、sleep、加锁帮助理解;真实实现不能留下“解锁后尚未睡眠”的丢失唤醒窗口,不应照这几行无保护伪代码自制CV。

p.10–12:signal、broadcast与结束生命周期

p.10 Purpose: wake up one thread waiting on a condition variable.

课件程序与接口(p.10,原页片段)

int pthread_cond_signal(pthread_cond_t *cond);

课件程序与接口(p.10,原页片段)

pthread_mutex_lock(&mutex);
// Change the condition
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

p.11 Purpose: wake up all threads waiting on a condition variable.

课件程序与接口(p.11,原页片段)

int pthread_cond_broadcast(pthread_cond_t *cond);

课件程序与接口(p.11,原页片段)

pthread_mutex_lock(&mutex);
// Change the condition
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);

p.12 Purpose: destroy a condition variable, releasing resources.

课件程序与接口(p.12,原页片段)

int pthread_cond_destroy(pthread_cond_t *cond);

课件程序与接口(p.12,原页片段)

pthread_cond_destroy(&cond);

signal让等待者有机会重新检查,broadcast让所有等待者如此。状态变化先在锁内完成,唤醒不是资源所有权移交。destroy只能在不再有等待或并发使用时做;释放CV对象与给等待线程发送退出协议是两回事。

p.13–14:原例是有界栈,不是FIFO队列

p.13 Problem: Manage multiple producer and consumer threads accessing a shared buffer. Producers add items to the buffer, while consumers remove them. The challenge is to synchronize access and avoid race conditions.

Solution: Use a mutex to protect the buffer and condition variables to signal when the buffer is not full (for producers) and not empty (for consumers). Producers wait if the buffer is full, and consumers wait if it's empty, ensuring safe and efficient access.

课件程序与接口(p.14,原页片段)

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 10
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_produce = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_consume = PTHREAD_COND_INITIALIZER;
int buffer[BUFFER_SIZE];
int count = 0;
void* producer(void* arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_produce, &mutex);
        }
        buffer[count++] = rand() % 100; // Produce item
        printf("Produced, count: %d\n", count);
        pthread_cond_signal(&cond_consume);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

课件程序与接口(p.14,原页片段)

void* consumer(void* arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        while (count == 0) {
            pthread_cond_wait(&cond_consume, &mutex);
        }
        int item = buffer[--count]; // Consume item
        printf("Consumed, count: %d\n", count);
        pthread_cond_signal(&cond_produce);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main() {
    pthread_t producers[2], consumers[2];
    for (int i = 0; i < 2; i++) {
        pthread_create(&producers[i], NULL, producer, NULL);
        pthread_create(&consumers[i], NULL, consumer, NULL);
    }
    for (int i = 0; i < 2; i++) {
        pthread_join(producers[i], NULL);
        pthread_join(consumers[i], NULL);
    }
    return 0;
}

原代码BUFFER_SIZE=10,producer写 buffer[count++]=...,consumer读 buffer[--count]。容量不变量是0≤count≤10;满时等cond_produce,空时等cond_consume,操作后通知另一类。

依原代码演算:依次生产10、20时数组前两项是10、20且count2,消费先--到1,取出20,再消费10。因此它是LIFO,不要因为叫buffer就称FIFO。两个生产者与两个消费者无限循环,main中的join正常不会全部返回;若要有限测试须设计停止条件和唤醒退出机制。

p.15–16:一次屏障的第五人唤醒前四人

p.15 Problem: In a barrier synchronization scenario, multiple threads must wait at a specific point until all threads reach that point. Only when all threads have reached the barrier can they all proceed.

Solution: Each thread increases a counter when it reaches the barrier. If not all threads have arrived, it waits. The last thread to arrive signals all waiting threads to continue.

课件程序与接口(p.16,原页片段)

#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 5

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_barrier = PTHREAD_COND_INITIALIZER;
int count = 0;

void* thread_func(void* arg) {
    int id = *(int*)arg;
    printf("Thread %d reached the barrier.\n", id);
    pthread_mutex_lock(&mutex);
    count++;
    if (count < NUM_THREADS) {
        pthread_cond_wait(&cond_barrier, &mutex);
    } else {
        pthread_cond_broadcast(&cond_barrier);
    }
    pthread_mutex_unlock(&mutex);
    printf("Thread %d passed the barrier.\n", id);
    return NULL;
}

课件程序与接口(p.16,原页片段)

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, thread_func, &thread_ids[i]);
    }
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}

p.16 Requirements:

Threads wait until all threads reach the point

Last thread signals all waiting threads

原题NUM_THREADS=5,count初值0,每个到达者在mutex内count++;前四个等待,第五个把count变5并broadcast,所有人才可越过。原代码用if,遇虚假唤醒可能提前通过,按本教程p.19的原则应以while(count<5)重查。

这是一次性屏障:count没有按代际复位。直接改成count=0也可能把尚未醒完的旧一轮线程和新一轮混起来;可复用屏障需要额外generation等状态(补充,不是原题已给的实现)。

补充:可复用屏障为什么要辨认“第几轮”。 一次屏障只问到达人数是否达到N。复用时,快线程可能完成第1轮后已经到第2轮,而慢线程仍从第1轮wait醒来。若最后到达者直接把count重置0,慢线程重查count<N又会睡下,把已经满足的旧轮当成尚未完成的新轮。

一种思路是增加generation:到达时记下自己所属代;最后一个到达者清本轮计数、推进代号并broadcast;其他线程等待“代号不再等于我进入时记录的那一代”。这是理解用机制说明,不是替原exercise填完整实现。计数与代号都要受同一mutex保护,且等待仍用while。

自测:把NUM_THREADS从5改成4,能否解决只成功创建4线程的所有情况?

不能在已有线程等待时随意改常量来修复。参与人数与创建失败协议应在启动/同步设计时一致确定;还要保证没有第五线程稍后加入错误轮次。固定屏障人数不是装饰参数,它描述必须到齐的参与者集合。

按原五线程例子走一遍:初始 count=0。到达者在 mutex 内依次把 count 改为 1、2、3、4,发现还未到 5,于是 wait 并释放锁。第五人把 count 改为 5 后 broadcast;前四人依次重新取得 mutex,确认 count 已到 5,再离开屏障。这里“都能继续”不表示五人同时拥有同一把 mutex,只表示每个人都不再被屏障条件挡住。

原示例用了 if (count < NUM_THREADS),应结合 p.19 的通用规则改为 while 重查,以免虚假唤醒让某人提前通过。这个 count 达到 5 后从未重置,因此它只实现一次性屏障;重复使用需要再区分轮次,不能简单让最后一人清零,否则上一轮尚未醒来的线程可能看到下一轮的 count 而再次睡眠。重复轮次属于补充边界,本页没有实现。

补充:屏障保证阶段先后,不保证下一阶段的互斥。 在正确的一次性五人屏障中,每个线程越过屏障之前,五人的“到达前工作”都已经完成。越过之后五人仍可以并发执行;若下一阶段共同写同一个变量,依然需要该阶段自己的同步。屏障不是一把自动覆盖后续所有代码的大锁。

也不要把打印位置当成计数已经更新的证明。原例先打印 reached,再加锁并递增 count;某线程打印后可能被切走,尚未登记到达。最后一人是实际把受保护的 count 更新到五的那个人,而不一定是你肉眼最后看到 reached 输出的线程。读并发日志时应问“这条输出位于状态改变前还是后”,才能把日志与算法事件对应起来。

p.17–18:哲学家练习先写状态约束

p.17 Problem: Five philosophers sit around a table with five forks. Each philosopher needs two forks to eat but only one is available on either side. The challenge is to avoid deadlock and ensure all philosophers eventually eat.

Solution: Use condition variables to manage fork availability. Philosophers check if both adjacent forks are available before picking them up. After eating, they put the forks back and signal neighbors, allowing them to eat if possible.

课件程序与接口(p.18,原页片段)

#include <pthread.h>
#include <stdio.h>
#define NUM_PHILOSOPHERS 5

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_vars[NUM_PHILOSOPHERS];
int state[NUM_PHILOSOPHERS];
enum { THINKING, HUNGRY, EATING };

void test(int i) {
    // TODO: Implement test function
    // Hint: Check if the philosopher can start eating
    // by ensuring neighbors are not eating.
}
void pickup_forks(int i) {
    pthread_mutex_lock(&mutex);
    // TODO: Implement pickup_forks function
    // Hint: Set state to HUNGRY and then test.
    // Wait if not able to eat.
    pthread_mutex_unlock(&mutex);
}
void return_forks(int i) {
    pthread_mutex_lock(&mutex);
    // TODO: Implement return_forks function
    // Hint: Set state to THINKING and test neighbors.
    pthread_mutex_unlock(&mutex);
}

课件程序与接口(p.18,原页片段)

void* philosopher(void* arg) {
    int i = *(int*)arg;
    while (1) {
        printf("Philosopher %d is thinking.\n", i);
        pickup_forks(i);
        printf("Philosopher %d is eating.\n", i);
        return_forks(i);
    }
    return NULL;
}
int main() {
    pthread_t philosophers[NUM_PHILOSOPHERS];
    int ids[NUM_PHILOSOPHERS];
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
        pthread_cond_init(&cond_vars[i], NULL);
        ids[i] = i;
        pthread_create(&philosophers[i], NULL, philosopher, &ids[i]);
    }
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
        pthread_join(philosophers[i], NULL);
    }
    for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
        pthread_cond_destroy(&cond_vars[i]);
    }
    return 0;
}

题面五人五叉,状态THINKING/HUNGRY/EATING,test检查左右邻居是否进餐;pickup先变HUNGRY再检查,不具备资格则等待自身CV;return变THINKING后重新测试邻居。必须始终保证相邻两人不同时EATING,所有状态检查修改在同一锁内。

原框架在同一循环中初始化第i个CV并启动线程i,早启动者可能触及尚未初始化邻居CV;更稳妥是先初始化全部同步对象和状态,再启动线程。该判断是框架审查,不替代练习TODO作答。无死锁还不自动证明每个人都不饿死。

把原 TODO 展开为可检查的状态过程(学习用伪代码):五个哲学家编号 0–4,左邻为 (i+4)%5,右邻为 (i+1)%5;所有 state 的检查与修改都在同一 mutex 内。

test(i):
  若 i 是 HUNGRY,且左邻、右邻都不是 EATING:
    先把 state[i] 改成 EATING
    再通知 cond_vars[i]
pickup_forks(i):
  加锁
  state[i] = HUNGRY
  test(i)
  只要 state[i] 还不是 EATING,就在自己的条件变量上等待并重查
  解锁
return_forks(i):
  加锁
  state[i] = THINKING
  test(左邻)
  test(右邻)
  解锁

例如 0 已在吃,则 1 和 4 不能被置为 EATING;2 与 0 不相邻,可以同时吃。0 放下后先改 THINKING,才可能允许 1 或 4 通过。通过者必须先在锁内被标成 EATING,再发通知,否则相邻线程可能在通知与实际执行之间同时误判可吃。

这保证相邻者不同时用同一把叉,并避免“每人持一叉等另一叉”的经典环;但若没有公平调度或等待顺序约束,不能据此证明每人一定在有界时间内吃到。原题的“ensure all philosophers eventually eat”还需要这一前进性条件。原 main 一边初始化条件变量一边创建线程,若补齐 test 后会访问邻居条件变量,应先完成五个条件变量及状态的初始化,再启动任何线程。原线程无限循环,因而正常路径上的 join 不会结束,后面的 destroy 也不会自然到达;完整可停止程序还需要结束协议。

p.19–20:用while与生命周期收束

p.19 Always lock the mutex before using condition variables.

Use while loops for pthread_cond_wait to handle spurious wakeups.

Ensure proper cleanup using pthread_cond_destroy.

p.19明确推荐while处理虚假唤醒,应据此修正屏障示意中的if,而不是把两页不一致忽略。p.20为Q&A。回看每个例子都应能回答:等什么、谁能改变、如何通知、何时能销毁。

本讲核心考点

  • [C] 解释wait为何需要mutex且必须循环检查。
  • [C] 根据count索引判断原缓冲为LIFO并推演容量。
  • [C] 推演五线程一次屏障,识别if和复用代际问题。
  • [C] 检查初始化完成与启动线程的先后关系。

附录

本份没有需要移至附录的长推导;计算过程已在对应页正文写全。

来源与证据

  • 原件:T05-条件变量-2025T2.pptx(本地课件);PDF副本(本地课件) p.1–20。本文页码均为 PDF 页号。

  • 考试证据:读了 课程大纲-2026T1.pdf、教学计划-2026T1.pdf、T08-期中参考-2025T2-题目-解答.pdf、T07-多级反馈队列-2025T2.pdf(p.13–15内嵌历史试题);未识别用途的 PDF:无

  • 2026-10-13 内容补全:保留原页顺序与版本,补入对应页的程序、接口、字段、条件和演算;原程序片段保留课件行号、拼写与省略处,不能据此视为经过编译的完整软件。本文已有运行记录属于其标注日期,本次未重新运行课程容器或提交作业。