旧游无处不堪寻
无寻处,惟有少年心
操作系统(五)

上一篇我们介绍了锁的概念,通过硬件和操作系统提供的能力来实现锁。然而,锁并不是并发程序设计所需的唯一原语。 具体来说,在很多情况下,线程需要检查某一条件(condition)满足之后,才会继续运行。例如线程的 Join 函数。

条件变量

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

信号量

信号量是有一个整数值的对象,可以用两个函数来操作它。在 POSIX 标准中,是 sem_wait() 和 sem_post():

#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1);

首先,sem_wait() 要么立刻返回(调用 sem_wait() 时,信号量的值大于等于 1),要么会让调用线程挂起,直到之后的一个 post 操作。当然,也可能多个调用线程都调用 sem_wait(),因此都在队列中等待被唤醒。
其次,sem_post() 并没有等待某些条件满足。它直接增加信号量的值,如果有等待线程,唤醒其中一个。
最后,当信号量的值为负数时,这个值就是等待线程的个数。

接下来我们介绍一些信号量的应用。

二值信号量(锁)

sem_t m;
sem_init(&m, 0, 1);
sem_wait(&m);

// critical section here

sem_post(&m);

上例是用信号量来实现锁。因为锁只有两个状态,所以这种用法有时也叫作二值信号量(binary semaphore)。

信号量用作条件变量

信号量也可以用在一个线程暂停执行,等待某一条件成立的场景。

sem_t s;

void *child(void *arg) {
printf("child\n");
sem_post(&s); // signal here: child is done
return NULL;
}

int main(int argc, char *argv[]) {
sem_init(&s, 0, 0);
printf("parent: begin\n");
pthread_t c;
Pthread_create(c, NULL, child, NULL);
sem_wait(&s); // wait here for child
printf("parent: end\n");
return 0;
}

实现信号量

利用底层的同步原语(锁和条件变量),来实现一个信号量:

typedef struct _Zem_t {
int value;
pthread_cond_t cond;
pthread_mutex_t lock;
} Zem_t;

// only one thread can call this
void Zem_init(Zem_t *s, int value) {
s->value = value;
Cond_init(&s->cond);
Mutex_init(&s->lock);
}

void Zem_wait(Zem_t *s) {
Mutex_lock(&s->lock);
while (s->value <= 0)
Cond_wait(&s->cond, &s->lock);
s->value--;
Mutex_unlock(&s->lock);
}

void Zem_post(Zem_t *s) {
Mutex_lock(&s->lock);
s->value++;
Cond_signal(&s->cond);
Mutex_unlock(&s->lock);
}