← 返回首页
读书笔记

《Operating System:Three Easy Pieces》第三十章 条件变量

锁并不是并发程序设计所需的唯一原语。在很多情况下,线程需要检查某一条件(condition)满足之后,才会继续运行。

c
volatile int done = 0; // 共享变量 void *child(void *arg) { printf("child\n"); // XXX how to indicate we are done? // 自旋锁标记 done = 1; return NULL; } int main(int argc, char *argv[]) { printf("parent: begin\n"); pthread_t c; Pthread_create(&c, NULL, child, NULL); // create child // XXX how to wait for child? // 加个自旋锁等待done为1 while (done == 0) ; // spin printf("parent: end\n"); return 0; }

尝试用一个共享变量。这种解决方案一般能工作,但是效率低下,因为主线程会自旋检查,浪费 CPU 时间。我们希望有某种方式让父线程休眠,直到等待的条件满足(即子线程完成执行)。

定义和程序

线程可以使用条件变量(condition variable),来等待一个条件变成真。

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

要声明这样的条件变量,只要像这样写:pthread_cond_t c;,这里声明 c 是一个条件变量(注意:还需要适当的初始化)。条件变量有两种相关操作:wait()和 signal()。线程要睡眠的时候,调用 wait()。当线程想唤醒等待在某个条件变量上的睡眠线程时,调用 signal()。

c
pthread_cond_wait(pthread_cond_t*c, pthread_mutex_t*m); pthread_cond_signal(pthread_cond_t*c);
c
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); // 这里先判断done防止done为1时永久休眠 while (done== 0) // 休眠,等待被唤醒 Pthread_cond_wait(&c,&m); Pthread_mutex_unlock(&m); } int main(int argc,char*argv[]) { printf("parent: begin\n"); pthread_t p; Pthread_create(&p, NULL, child, NULL); thr_join(); printf("parent: end\n"); return 0; }

wait()调用有一个参数,它是互斥量

它假定在 wait()调用时,这个互斥量是已上锁状态。wait()的职责是释放锁,并让调用线程休眠(原子地)。当线程被唤醒时(在另外某个线程发信号给它后),它必须重新获取锁,再返回调用者。

提示:发信号时总是持有锁

生产者/消费者(有界缓冲区)问题

c
cond_t cond; mutex_t mutex; void* producer(void*arg) { int i; for (i= 0; i < loops; i++) { Pthread_mutex_lock(&mutex);// p1 if (count == 1) // p2 Pthread_cond_wait(&cond,&mutex);// p3 put(i);// p4 Pthread_cond_signal(&cond);// p5 Pthread_mutex_unlock(&mutex);// p6 } } void* consumer(void*arg) { int i; for (i= 0; i< loops; i++) { Pthread_mutex_lock(&mutex); // c1 // 1. 此处存在问题,当有两个消费者(Cus1,Cus2)时,其中Cus1进入休眠, // Cus2正好在生产者生产后执行get把count消费掉,此时 // 生产者又调用signal唤醒Cus1休眠的,直接执行了get发现 // 没有count了,所以要把if改成while,wait出来还要再 // 判断一次count // 2. 使用while带来另一个问题,Cus1消费后本该唤醒生产者, // 但如果唤醒了Cus2线程,因为没东西消费,Cus2也会等待, // 就会导致三个线程都在等待中。解决方法是设置两个条件变量, // 保证消费者唤醒生产者,生产者唤醒消费者 if (count== 0)// c2 Pthread_cond_wait(&cond,&mutex);// c3 int tmp= get();// c4 Pthread_cond_signal(&cond);// c5 Pthread_mutex_unlock(&mutex);// c6printf("%d\n", tmp); } }

最终代码:

c
int buffer[MAX]; int fill = 0; int use = 0; int count = 0; void put(int value) { buffer[fill] = value; fill = (fill + 1) % MAX; count++; } int get() { int tmp = buffer[use]; use = (use + 1) % MAX; count --; return tmp; } cond_t empty, fill; mutex_t mutex; void *producer(void *arg) { int i; for (i = 0; i < loops; i++) { Pthread_mutex_lock(&mutex); // p1 while (count == MAX) // p2 Pthread_cond_wait(&empty, &mutex); // p3 put(i); // p4 Pthread_cond_signal(&fill); // p5 Pthread_mutex_unlock(&mutex); // p6 } } void *consumer(void *arg) { int i; for (i = 0; i < loops; i++) { Pthread_mutex_lock(&mutex); // c1 while (count == 0) // c2 Pthread_cond_wait(&fill, &mutex); // c3 int tmp = get(); // c4 Pthread_cond_signal(&empty); // c5 Pthread_mutex_unlock(&mutex); // c6 printf("%d\n", tmp); } }

覆盖条件

以下代码用于内存分配管理,free 后会唤醒 allocate 时因空间不够而等待的线程

c
// how many bytes of the heap are free?int bytesLeft= MAX_HEAP_SIZE; // need lock and condition toocond_t c; mutex_t m; void* allocate(int size) { Pthread_mutex_lock(&m); while (bytesLeft< size) Pthread_cond_wait(&c,&m); void* ptr= ...;// get mem from heapbytesLeft-= size; Pthread_mutex_unlock(&m); return ptr; } void free(void* ptr,int size) { Pthread_mutex_lock(&m); bytesLeft += size; Pthread_cond_signal(&c);// whom to signal??Pthread_mutex_unlock(&m); }

pthread_cond_broadcast()代替上述代码中的pthread_cond_signal(),唤醒所有的等待线程。这样做,确保了所有应该唤醒的线程都被唤醒。

当然,不利的一面是可能会影响性能,因为不必要地唤醒了其他许多等待的线程,它们本来(还)不应该被唤醒。这些线程被唤醒后,重新检查条件,马上再次睡眠。

参考

本文由 GJJ 创作,内容来源于 Notion 数据库,随时可在 Notion 中编辑更新。 本站由 DeepSeek-v4-flash 辅助构建,项目参考 NotionNext

← 返回首页
61
文章
6
标签
3
分类
962
运行天数