锁并不是并发程序设计所需的唯一原语。在很多情况下,线程需要检查某一条件(condition)满足之后,才会继续运行。
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()。
pthread_cond_wait(pthread_cond_t*c, pthread_mutex_t*m);
pthread_cond_signal(pthread_cond_t*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()的职责是释放锁,并让调用线程休眠(原子地)。当线程被唤醒时(在另外某个线程发信号给它后),它必须重新获取锁,再返回调用者。
提示:发信号时总是持有锁
生产者/消费者(有界缓冲区)问题
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);
}
}最终代码:
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 时因空间不够而等待的线程
// 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(),唤醒所有的等待线程。这样做,确保了所有应该唤醒的线程都被唤醒。
当然,不利的一面是可能会影响性能,因为不必要地唤醒了其他许多等待的线程,它们本来(还)不应该被唤醒。这些线程被唤醒后,重新检查条件,马上再次睡眠。