操作系统导论第30章——条件变量总结

本文详细探讨了如何利用条件变量和信号量在多线程中实现父线程等待子线程的join功能,以及在生产者消费者问题中的应用。重点讲解了条件变量的工作原理、使用场景及解决竞态条件的方法,包括为何使用while循环和信号量的正确使用策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 提出背景

如何实现父线程等待子线程的join功能?

基于自旋的方式可以实现,代码如下:

#include <pthread.h>
#include <cstdio>

volatile int done = 0;

void* child(void* arg) {
  printf("child\n");
  done = 1;
  return NULL;
}

int main() {
  printf("parent:begin\n");
  pthread_t c;
  pthread_create(&c, NULL, child, NULL);
  while (done == 0)
    ;
  printf("parent: end\n");
  return 0;
}

分析:可以实现,不过通过自旋的方式,浪费CPU时间,非常低效

解决办法:条件变量 

2. 条件变量定义

线程可以使用条件变量(condition variable)来等待一个条件变成真。条件变量是一个显式队列,当某些执行条件(即条件,condition)不满足时,线程可以把自己加入队列,等待该条件。另外某个线程,当它改变了上述状态时,就可以唤醒一个或者多个等待线程(通过在该条件上发信号),让它们继续执行。

核心观点:当前线程条件不满足则休眠等待,不占用CPU时间;满足条件则发送信号,唤醒等待线程。

3. Pthread基本语法

3.1 初始化信号量

pthread_cond_t c = PTHREAD_COND_INITIALIZER;

3.2 休眠等待信号

pthread_cond_wait(pthread_cond_t* c, pthread_mutex_t* m);

参数都是指针,除了信号量,还有锁(Pthread中称为互斥量) 

 3.3 发送信号唤醒等待线程

pthread_cond_signal(pthread_cond_t* c);

参数为信号量指针

 

由此解决背景中的问题,基于信号量的解决方案

#include <cstdio>
#include <pthread.h>


int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;

void thr_exit() {
  pthread_mutex_lock(&m);
  done = 1;
  pthread_cond_signal(&c);
  pthread_mutex_unlock(&m);
}

void* child(void* arg) {
  printf("child\n");
  thr_exit();
  return NULL;
}

void thr_join() {
  pthread_mutex_lock(&m);
  while (done == 0)
    pthread_cond_wait(&c, &m);
  pthread_mutex_unlock(&m);
}

int main() {
  printf("parent: begin\n");
  pthread_t p;
  pthread_create(&p, NULL, child, NULL);
  thr_join();
  printf("parent: end\n");
  return 0;
}

分析:

1. 为什么要使用done? 

答:done实现了不同线程之间的交流,睡眠,唤醒和锁都离不开它。举个没有done的反例,子线程先运行thr_exit中的pthread_cond_signal发送信号,父线程后运行,若没有done,直接调用pthread_cond_wait,则将永远醒不来。

2. 为什么要有锁?

答:为了避免竞态条件,举个反例,若父线程先运行thr_join,执行到done == 0成立,该要运行pthread_cond_wait被系统中断运行子线程,由于没有锁子线程可以修改done,修改为1,然后切换到父线程,由于不会再次判断条件,则父线程运行pthread_cond_wait,则父线程将永远不会被唤醒。

经验总结:发信号时总是持有锁

3. 生产者/消费者问题(有界缓冲器)

3.1 问题描述

假设有一个或多个生产者线程和一个或多个消费者线程。生产者把生成的数据项放入缓冲区;消费者从缓冲区取走数据项,以某种方式消费。

3.2 代码

#include <pthread.h>
#include <cstdio>

#define MAX 10
int buffer[MAX];
int fill_ptr = 0;
int use_ptr = 0;
int count = 0;

pthread_cond_t empty, fill;
pthread_mutex_t mutex;

void put(int value) {
  buffer[fill_ptr] = value;
  fill_ptr = (fill_ptr + 1) % MAX;
  count++;
}

int get() {
  int tmp = buffer[use_ptr];
  use_ptr = (use_ptr + 1) % MAX;
  count--;
  return tmp;
}

void* producer(void* arg) {
  size_t loops = (size_t) arg; //注意参数类型转换
  for (int i = 0; i < loops; i++) {
    pthread_mutex_lock(&mutex);
    while (count == MAX)
      pthread_cond_wait(&empty, &mutex);
    put(i);
    pthread_cond_signal(&fill);
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}

void* consumer(void* arg) {
  size_t loops = (size_t) arg;
  for (int i = 0; i < loops; i++) {
    pthread_mutex_lock(&mutex);
    while (count == 0)
      pthread_cond_wait(&fill, &mutex);
    int tmp = get();
    pthread_cond_signal(&empty);
    pthread_mutex_unlock(&mutex);
    printf("%d\n", tmp);
  }
  return NULL;
}

 分析

1. 为什么判断条件用while不用if ?

答:考虑有两个消费者线程,一个生成者线程的情况,如果一个消费者线程1由于资源不足等待信号,接着生产者线程生成资源发生信号,消费者线程进入就绪状态,但是这个时候中断发生,消费者线程2抢先执行并且消费了资源,然后再切换到消费者线程1,由于使用if只判断一次,不用再判断是否有资源,消费者执行消费操作,但是资源已经没有了,出现错误。

2. 为什么需要两个条件变量

答:如果只有一个条件变量,消费者线程除唤醒生产者线程外还可以唤醒其他消费者线程,同样的生产者线程除了唤醒消费者线程也可以唤醒其他生产者线程。举个反例,如果有一个生产者线程,两个消费者线程,首先生成者线程生成了一个资源,然后唤醒消费者线程1,消费了资源,接着消费者发送信号,此时有可能唤醒生成者线程(正确)也可能唤醒消费者线程2(错误),如果唤醒了消费者线程2,它发现没有资源了于是也进入休眠,那么此时三个线程都进入了休眠,将全部无法醒过来!!

3. 为什么需要设置loops?

答:这样可以使得生成者线程可以一次性生成多个资源,消费者线程可以一次性消费多个资源,提高了并发度

经验总结:条件变量的时候while循环判断

4. 覆盖条件

这里主要谈到如果无法判断要唤醒哪一个线程,那么可以唤醒所有的线程,使用pthread_cond_broadcast()可以实现,当然这种方式会影响性能,应该尽量避免,但是有时候也是一种选择(比如简单多线程内存分配库再内存资源不足后调用处理程序,现在有了新的内存空间,那么该唤醒哪一个之前无法获取内存的线程呢,作者是直接使用唤醒所有线程)。

5. 问题扩展

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值