首页 > 操作系统 > 用于线程同步的条件变量(Condition Variables)

用于线程同步的条件变量(Condition Variables)

2010年2月24日 发表评论 阅读评论

  条件变量(condition variables或condvars)与前面讲的睡眠锁(sleepon lock)非常类似。而实际上睡眠锁是在条件变量的基础上构建的,这也是为什么我们在睡眠锁的例子的解释表中有一个CONDVAR状态。它也能通过不停的调用pthread_cond_wait()函数来释放互斥体、等待以及重新获取互斥体,和pthread_sleepon_wait()函数一样。

  下面我们就略过初始化的步骤,并使用条件变量来重新完成sleepon部分的那个生产者与消费者的多线程的程序。之后再讨论调用的函数。

 

/*
 * cp1.c
*/

#include <stdio.h>
#include <pthread.h>

int data_ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  condvar = PTHREAD_COND_INITIALIZER;

void *
consumer (void *notused)
{
    printf ("In consumer thread...\n");
    while (1) {
        pthread_mutex_lock (&mutex);
        while (!data_ready) {
            pthread_cond_wait (&condvar, &mutex);
        }
        // process data
        printf ("consumer:  got data from producer\n");
        data_ready = 0;
        pthread_cond_signal (&condvar);
        pthread_mutex_unlock (&mutex);
    }
}

void *
producer (void *notused)
{
    printf ("In producer thread...\n");
    while (1) {
        // get data from hardware
        // we'll simulate this with a sleep (1)
        sleep (1);
        printf ("producer:  got data from h/w\n");
        pthread_mutex_lock (&mutex);
        while (data_ready) {
            pthread_cond_wait (&condvar, &mutex);
        }
        data_ready = 1;
        pthread_cond_signal (&condvar);
        pthread_mutex_unlock (&mutex);
    }
}

main ()
{
    printf ("Starting consumer/producer example...\n");

    // create the producer and consumer threads
    pthread_create (NULL, NULL, producer, NULL);
    pthread_create (NULL, NULL, consumer, NULL);

    // let the threads run for a bit
    sleep (20);
}

  可以看出来这个例子与前面的睡眠锁的例子非常相似,只有几个地方有变化(我们添加的一些printf()函数以及一个main()函数,以让这个程序能够运行!)我们马上看到的第一个就是一个新的数据类型pthread_cond_t,它是条件变量的声明。

  另外一个会注意到的是消费者线程的结构与前面的sleepon例子程序中的一样。我们只不过使用了标准的互斥体函数pthread_mutex_lock()与pthread_mutex_unlock()替换了原来对应的sleepon函数。而pthread_sleepon_wait()函数被pthread_cond_wait()函数所替换。这两个函数之间的不同就是pthread_sleepon_wait()函数封装了一个互斥体在它里面,而新的函数使用了条件变量,也就是直接使用了互斥体。使用这种方法就是有更大的灵活性。

  最后,可能注意到的就是用pthread_cond_signal()替换了pthread_sleepon_signal()函数。

信号(Signal)与广播(Broadcast)

  我们要谈谈pthread_cond_signal()函数与pthread_cond_broadcast()函数之间的区别。

  简单来说,信号版本的函数只会唤醒一个线程。如果有多个线程阻塞于等待状态,如果一个线程发了信号,那么这些线程中只能有一个线程会被唤醒。被唤醒的这个是优先级最高的那个。如果有2个以上的线程在同一优先级,唤醒的次序就不可预料了。如果使用广播版本的函数,所有的阻塞线程都会被唤醒。

  看起来唤醒全部的线程显得有点浪费。不过如果只唤醒一个随机的线程则显得比较草率。

  这样的话,我们就必须根据情况来使用这两个函数。如果我们只有一个线程等待,显然一个信号函数就可以了,因为只有一个线程在等待并且我们只唤醒一个线程。

  在多个线程的情况下,我们必须搞清楚这些线程等待的原因。一般来说线程等待有两种情况:

  • 所有的线程都可以看为是等同的并且它们有效的形成了一个线程池,在这个池中的线程都准备好来处理某些类型的请求;
  • 所有的线程都是不同的并且每个线程都在等待一个特殊情况的发生。

  在第一种情况下,我们可以想象所有的线程可能有像下面这样的代码:

/*
 * cv1.c
*/

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex_data = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv_data = PTHREAD_COND_INITIALIZER;
int data;

thread1 ()
{
    for (;;) {
        pthread_mutex_lock (&mutex_data);
        while (data == 0) {
            pthread_cond_wait (&cv_data, &mutex_data);
        }
        // do something
        pthread_mutex_unlock (&mutex_data);
    }
}

// thread2, thread3, etc have the identical code.

  在这个情况下,到底是哪个线程获取了数据是没有区别的,只要这个线程获取数据后对其做一些工作就行了。

  但是如果情况像下面这样,那就不一样了。

/*
 * cv2.c
*/

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex_xy = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cv_xy = PTHREAD_COND_INITIALIZER;
int x, y;

int isprime (int);

thread1 ()
{
    for (;;) {
        pthread_mutex_lock (&mutex_xy);
        while ((x > 7) && (y != 15)) {
            pthread_cond_wait (&cv_xy, &mutex_xy);
        }
        // do something
        pthread_mutex_unlock (&mutex_xy);
    }
}

thread2 ()
{
    for (;;) {
        pthread_mutex_lock (&mutex_xy);
        while (!isprime (x)) {
            pthread_cond_wait (&cv_xy, &mutex_xy);
        }
        // do something
        pthread_mutex_unlock (&mutex_xy);
    }
}

thread3 ()
{
    for (;;) {
        pthread_mutex_lock (&mutex_xy);
        while (x != y) {
            pthread_cond_wait (&cv_xy, &mutex_xy);
        }
        // do something
        pthread_mutex_unlock (&mutex_xy);
    }
}

  在现在的情况下,只唤醒一个线程是不够的。我们必须唤醒全部三个线程并让它们检查其判定条件是否满足。

  这也清楚的演示了我们上面所说的线程等待的原因。由于这些线程等待的都是不同的条件(线程1等待的是x值小于或等于7或y值等于15,线程2等待x值为一个素数,线程3等待x值等于y),这样我们就没有别的选择,只能把它们都唤醒。

睡眠锁(Sleepon)与条件变量(condvars)

  睡眠锁对条件变量有一个重要的优势。假设你要同步多个对象。如果使用条件变量,一般来说每个对象要绑定一个条件变量。这样的话,如果你有M个对象,你可能就需要M的条件变量。如果使用睡眠锁的话,线程等待特定对象的底层的条件变量(睡眠锁是基于条件变量实现的)是动态分配的。这样的话,对M个对象使用睡眠锁而有N个线程被阻塞,那么你最多有N个条件变量(而不是M个)。

  不过,条件变量要比睡眠锁更灵活些,因为:

  1. 睡眠锁是构建于条件变量之上的;
  2. 睡眠锁内部在库中封装了互斥体,而条件变量可以让你明确的指定互斥体。

  第一点可能看起来颇有争议性。第二点则很有意义。当互斥体被封装在库中的时候,就意味着在一个进程中只能有一个互斥体,而不会受进程中的线程数或数据变量组的数目的影响。这可能是一个有限制的因素,特别是当你考虑到需要使用这个唯一的互斥体来让进程中的任何线程来访问任意或全部数据变量的时候。

  好些的设计就是使用多个互斥体,每个互斥体对应一组数据,并在必要的时候使用条件变量将它们绑定。这种方法的优势与危险就是在程序编译与运行时不会有任何的检查来确保你能够:

  • 在操作变量之前已经锁定了互斥体;
  • 是否对特定的变量使用了正确的互斥体;
  • 是否使用了有合适的互斥体与变量的正确的条件变量。

  避免这些问题的最简单的办法就是要有好的设计与设计评估,并使用面向对象的编程技术。当然了,你根据你自己的编程风格以及程序的运行效率的需求来选择使用一个或全部的解决方法。

  使用条件变量要记住的要点就是:

  1. 互斥体是用于测试与访问变量;
  2. 条件变量是用于集合点。

  下面是图示:

dpt9

一对一互斥体以及条件变量关联

  由于没有检查,你可以将一组变量关联到互斥体“ABC”并将另一组变量关联到互斥体“DEF”,并将这两组变量关联到条件变量“ABCDEF”,如下图所示:

dpt10

  这很有用。由于互斥体是用来访问与检测,也就是说我们在访问某个变量是都要选择正确的互斥体。如果我要检查变量C,我们就要先锁定互斥体“互斥体ABC”。那我们要改变变量E的值该怎么办?在我们改变它的值之前,先要获取互斥体“互斥体DEF”。之后,我们修改它,再传球给条件变量“条件变量ABCDEF”让其告诉大家变量已经改变。在此之后,我们就释放互斥体。

  现在,想象一下会发生什么。一下子我的那些等待“条件变量ABCDEF”的线程都会被唤醒。等待函数会尝试获取那个互斥体。这里的临界点就是有两个互斥体可被获取。也就是说,在一个SMP系统上,两个并发流的线程可以运行,每个线程都会使用独立的互斥体检查在它们看来是独立的变量。是不是有点酷?

Related posts:

  1. 用于线程同步的Sleepon锁
  2. 多线程中壁垒(barrier)的使用
  3. 线程的启动
  4. 线程池(Pools of threads)
  5. 进程的启动

  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.