学科分类
目录
Linux编程

条件变量

使用条件变量控制线程同步时,线程访问共享资源的前提,是程序中设置的条件变量得到满足。条件变量不会对共享资源加锁,但也会使线程阻塞,若线程不满足条件变量规定的条件,就会进入阻塞状态直到条件满足。

条件变量往往与互斥锁搭配使用,在线程需要访问共享资源时,会先绑定一个互斥锁,然后检测条件变量,若条件变量满足,线程就继续执行,并在资源访问完成后解开互斥锁;若条件变量不满足,线程将解开互斥锁,进入阻塞状态,等待条件变量状况发生改变。一般条件变量的状态由其它非阻塞态的线程改变,条件变量被满足则会唤醒阻塞中的进程,这些线程再次争夺互斥锁,对条件变量状况进行测试。

综上所述,条件变量的使用分为以下四个步骤:

(1)初始化条件变量;

(2)等待条件变量满足;

(3)唤醒阻塞线程;

(4)释放条件变量。

针对以上步骤,Linux系统中提供了一组与条件变量相关的系统调用,此组系统调用都存在于函数库pthread.h中,下面将对这些系统调用进行讲解。

① 初始化条件变量

Linux系统中用于初始化条件变量的函数为pthread_cond_init(),其声明如下:

int pthread_cond_init(pthread_cond_t *restrict cond,
​             const pthread_condattr_t *restrict attr);

函数pthread_cond_init()中的参数cond代表条件变量,本质是一个指向pthread_cond_t类型的结构体指针,pthread_cond_t是Linux系统中定义的条件变量类型。参数attr代表条件变量的属性,通常设置为NULL,表示使用默认属性初始化条件变量,其默认值为PTHREAD_PROCESS_PRIVATE,表示当前进程中的线程共用此条件变量;也可将attr设置为PTHREAD_PROCESS_SHARED,表示多个进程间的线程共用条件变量。

若函数pthread调用成功则返回0,否则返回-1,并设置errno。

除使用函数pthread_cond_init()动态初始化条件变量外,也可以使用如下语句以静态方法初始化条件变量:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

此种方式与将attr参数初始化为NULL的pthread_cond_init()函数等效,但是不进行错误检查。

② 阻塞等待条件变量

Linux系统中一般通过pthread_cond_wait()函数,使线程进入阻塞状态,等待一个条件变量,其声明如下:

int pthread_cond_wait(pthread_cond_t *restrict cond,
​              pthread_mutex_t *restrict mutex);

函数pthread_cond_wait()中的参数cond代表条件变量;参数mutex代表与当前线程绑定的互斥锁。若该函数调用成功则返回0,否则返回-1,并设置errno。

pthread_cond_wait()类似于互斥锁中的函数pthread_mutex_lock(),但其功能更为丰富,它的工作机制如下:

(1)阻塞等待条件变量cond满足;

(2)解除已绑定的互斥锁(类似于pthread_mutex_unlock());

(3)当线程被唤醒,pthread_cond_wait()函数返回,pthread_cond_wait()函数同时会解除线程阻塞,并使线程重新申请绑定互斥锁。

以上工作机制中,前两条为一个原子操作;需要注意的最后一条,最后一条机制表明:当线程被唤醒后,仍需重新绑定互斥锁。这是因为,“线程被唤醒”及“绑定互斥锁”并不是一个原子操作,条件变量满足后也许会有多个处于运行态的线程出现,并竞争互斥锁,极有可能在线程B绑定互斥锁之前,线程A已经执行了以下操作:获取互斥锁——修改条件变量——解除互斥锁,此时线程B即便获取到互斥锁,条件变量仍不满足,线程B应继续阻塞等待。综上所述,再次检测条件变量的状况是极有必要的。

如图1展示了条件变量机制控制程序逻辑流程的示意图。

图1 条件变量控制流程示意图

除pthread_cond_wait()外,pthread_cond_timedwait()也能使线程阻塞等待条件变量,不同的是,该函数可以指定线程的阻塞时长,若等待超时,该函数便会返回。函数pthread_cond_timedwait()存在于函数库pthread.h中,其声明如下:

int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                                pthread_mutex_t *restrict mutex,
                                const struct timespec *restrict abstime);

函数pthread_cond_timedwait()中的参数cond代表条件变量,参数mutex代表互斥锁,参数abstime代表绝对时间,用于设置等待时长,该参数是一个传入参数,本质是一个struct timespec类型的结构体指针,该结构体的定义如下:

struct timespec {
  time_t tv_sec;   //秒
  long  tv_nsec;   //纳秒
}

① pthread_cond_signal()

pthread_cond_signal()函数会在条件变量满足之后,以信号的形式唤醒阻塞在该条件变量的一个线程。处于阻塞状态中的线程的唤醒顺序由调度策略决定。pthread_cond_signal()函数存在于函数库pthread.h中,其声明如下:

int pthread_cond_signal(pthread_cond_t *cond);

函数pthread_cond_signal()中的参数cond代表条件变量,若该函数调用成功则返回0,否则返回-1,并设置errno。

② pthread_cond_broadcast()

pthread_cond_broadcast()函数同样唤醒阻塞在指定条件变量的线程,不同的是,该函数会以广播的形式,唤醒阻塞在该条件变量上的所有线程。pthread_cond_broadcast()函数存在于函数库pthread.h中,其声明如下:

int pthread_cond_broadcast(pthread_cond_t *cond);

函数pthread_cond_broadcast()中的参数cond代表条件变量,若该函数调用成功则返回0,否则返回-1,并设置errno。

下面通过一个案例来展示使用条件变量实现线程同步的方法。

案例10:生产者-消费者模型是线程同步中的一个经典案例。假设有两个线程,这两个线程同时操作一个共享资源(一般称为汇聚),其中一个模拟生产者行为,生产共享资源,当容器存满时,生产者无法向其中放入产品;另一个线程模拟消费者行为,消费共享资源,当产品数量为0时,消费者无法获取产品,应阻塞等待。显然,为防止数据混乱,每次只能由生产者、消费者中的一个,操作共享资源。本案例要求使用程序实现简单的生产者-消费者模型(可假设容器无限大)。

案例实现如下:

 1    #include <stdio.h>
 2    #include <stdlib.h>
 3    #include <unistd.h>
 4    #include <pthread.h>
 5    struct msg {
 6        struct msg *next;
 7        int num;
 8    };
 9    struct msg *head;
 10    pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;    //初始化条件变量
 11    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;        //初始化互斥锁
 12    //消费者
 13    void *consumer(void *p)
 14    {
 15        struct msg *mp;
 16        for (;;) {
 17            pthread_mutex_lock(&lock);                        //加锁
 18            //若头结点为空,表明产品数量为0,消费者无法消费产品
 19            while (head == NULL) {
 20                pthread_cond_wait(&has_product, &lock);        //阻塞等待并解锁
 21            }
 22            mp = head; 
 23            head = mp->next;                        //模拟消费一个产品
 24            pthread_mutex_unlock(&lock);
 25            printf("-Consume ---%d\n", mp->num);
 26            free(mp);
 27            sleep(rand() % 5);
 28        }
 29    }
 30    //生产者
 31    void *producer(void *p)
 32    {
 33        struct msg *mp;
 34        while (1) {
 35            mp = malloc(sizeof(struct msg));
 36            mp->num = rand() % 1000 + 1;            //模拟生产一个产品
 37            printf("-Produce ---%d\n", mp->num);
 38            pthread_mutex_lock(&lock);            //加锁
 39            mp->next = head;                        //插入结点(添加产品)
 40            head = mp;
 41            pthread_mutex_unlock(&lock);            //解锁
 42            pthread_cond_signal(&has_product);      //唤醒等待在该条件变量上的一个线程
 43            sleep(rand() % 5);
 44        }
 45    }
 46    int main(int argc, char *argv[])
 47    {
 48        pthread_t pid, cid;
 49        srand(time(NULL));
 50        //创建生产者、消费者线程
 51        pthread_create(&pid, NULL, producer, NULL);
 52        pthread_create(&cid, NULL, consumer, NULL);
 53        //回收线程
 54        pthread_join(pid, NULL);
 55        pthread_join(cid, NULL);
 56        return 0;
 57    }

案例10中第5~8行代码定义了一个链表结点,用于存储生产者线程创建的资源;第9行代码定义了链表的头结点,该链表是一个全局变量,因此是所有线程都可访问的公有资源;第10、11两行分别定义并初始化了互斥锁与条件变量;第13~29行代码中的函数consumer()用于模拟消费者的行为;第31~45行代码中的函数producer()用于模拟生产者的行为;第46~57行代码为主程序,主要用于创建生产者、消费者线程,和执行线程的回收工作。

由于本案例中未对容器容量进行限制,因此生产者只要能获取互斥锁,便能成功生产产品;但对消费者来说,总需先有产品才能消费,因此,无论案例执行多少次,第一行打印的总是生产者信息。编译案例10,执行程序,执行结果如下:

-Produce ---950
-Consume ---950
-Produce ---741
-Produce ---16
-Produce ---136
-Produce ---196
-Consume ---196
-Produce ---697
-Consume ---697
-Consume ---136
……

观察程序执行结果,由执行结果可知案例10实现成功。

在生产者-消费者模型中,生产者、消费者线程除受互斥锁限制,不能同时操作共享资源外,还受到条件变量的限制:对生产者线程而言,若共享资源区已满,生产者便无法向其中放入数据;对消费者线程而言,若共享资源区为空,消费者便无法从其中获取数据。

若在实现生产者-消费者模型时,创建了多个消费者线程,且程序中只使用互斥锁限制线程,那么不但生产者与消费者之间会竞争互斥锁,不同的消费者同样会竞争互斥锁;而添加条件变量后,只有在满足读取条件时,消费者之间才会产生竞争关系。由此可知,相比互斥锁,条件变量有效减少了线程间的竞争。读者可自行修改案例10代码,观察程序执行结果,对此进行验证。

点击此处
隐藏目录