Linux线程同步(4)——条件变量

        本文讨论第二种线程同步的方法——条件变量。

        条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:

  • 一个线程等待某个条件满足而被阻塞;
  • 另一个线程中,条件满足时发出“信号”。 为了说明这个问题,来看一个没有使用条件变量的例子,生产者——消费者模式,生产者这边负责生产产品、而消费者负责消费产品,对于消费者来说,没有产品的时候只能等待产品出来,有产品就使用它。

        这里我们使用一个变量来表示这个这个产品,生产者生产一件产品变量加 1,消费者消费一次变量减 1, 示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex;
static int g_avail = 0;

static void *consumer_thread(void *arg){
    for (;;) {
        pthread_mutex_lock(&mutex);//互斥锁上锁
        while(g_avail > 0)
            g_avail--;              //消费
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[]){
    pthread_t tid;
    int ret;

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex,NULL);
    
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 生产 */
    for(;;){
        pthread_mutex_lock(&mutex);//互斥锁上锁
        g_avail++;              
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    
    // /* 销毁互斥锁 */
    // pthread_mutex_destroy(&mutex);
    exit(0);
}

        此代码中,主线程作为“生产者”,新创建的线程作为“消费者”,运行之后它们都回处于死循环中, 所以代码中没有加入销毁互斥锁、等待回收新线程相关的代码,进程终止时会自动被处理。

        上述代码虽然可行,但由于新线程中会不停的循环检查全局变量 g_avail 是否大于 0,故而造成 CPU 资源的浪费。采用条件变量这一问题就可以迎刃而解!条件变量允许一个线程休眠(阻塞等待)直至获取到另 一个线程的通知(收到信号)再去执行自己的操作,譬如上述代码中,当条件 g_avail > 0 不成立时,消费者线程会进入休眠状态,待生产者生成产品后( g_avail > 0条件成立时),向处于等待状态的线程发出“信号”,其它线程收到“信号”之后,便会被唤醒!

        Tips:这里提到的信号并不是第八章内容所指的信号,需要区分开来!

        前面说到,条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。

条件变量初始化

        条件变量使用 pthread_cond_t 数据类型来表示,类似于互斥锁,在使用条件变量之前必须对其进行初始化。初始化方式同样也有两种:使用宏 PTHREAD_COND_INITIALIZER 或者使用函数 pthread_cond_init(), 使用宏的初始化方法与互斥锁的初始化宏一样,这里就不再重述!譬如:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

        pthread_cond_init()函数原型如下所示:

#include <pthread.h>

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

        使用 pthread_cond_init()函数初始化条件变量,当不再使用时,使用 pthread_cond_destroy() 销毁条件变量。

        参数 cond 指向 pthread_cond_t 条件变量对象,对于 pthread_cond_init()函数,类似于互斥锁,在初始化条件变量时设置条件变量的属性,参数 attr 指向一个 pthread_condattr_t 类型对象,pthread_condattr_t 数据类型用于描述条件变量的属性。可将参数 attr 设置为 NULL,表示使用属性的默认值来初始化条件变量,与使用 PTHREAD_COND_INITIALIZER 宏相同。

        函数调用成功返回 0,失败将返回一个非 0 值的错误码。

        对于初始化与销毁操作,有以下问题需要注意:

  • 在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或者函数 pthread_cond_init()都行;
  • 对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
  • 对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
  • 对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
  • 经 pthread_cond_destroy()销毁的条件变量,可以再次调用 pthread_cond_init()对其进行重新初始化。

通知和等待条件变量

        条件变量的主要操作便是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。

        函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。 pthread_cond_signal()和 pthread_cond_broadcast()函数原型如下所示:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

        参数 cond 指向目标条件变量,向该条件变量发送信号。调用成功返回 0;失败将返回一个非 0 值的错误码。

        pthread_cond_signal()和 pthread_cond_broadcast()的区别在于:二者对阻塞于 pthread_ cond_wait()的多个线程对应的处理方式不同,pthread_cond_signal()函数至少能唤醒一个线程,而 pthread_cond_broadcast()函数则能唤醒所有线程。使用 pthread_cond_broadcast()函数总能产生正确的结果,唤醒所有等待状态的线程,但函数 pthread_cond_signal()会更为高效,因为它只需确保至少唤醒一个线程即可,所以如果我们的程序当中, 只有一个处于等待状态的线程,使用 pthread_cond_signal()更好,具体使用哪个函数根据实际情况进行选择!

        pthread_cond_wait()函数原型如下所示:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)

        当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待状态(阻塞)。pthread_cond_wait()函数包含两个参数:

        cond:指向需要等待的条件变量,目标条件变量;         

        mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;前面开头便给大家介绍 了,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。

        返回值:调用成功返回 0;失败将返回一个非 0 值的错误码。 在 pthread_cond_wait()函数内部会对参数 mutex 所指定的互斥锁进行操作,通常情况下,条件判断以及 pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。调用 pthread_cond _wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表 上,然后将互斥锁解锁;当 pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁。

        值得注意的是,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。如果调用 pthread_cond_signal()和 pthread_cond_broadcast()向指定条件变量发送信号时,若无任何线程等待该条件变量, 这个信号也就会不了了之。

        当调用 pthread_cond_broadcast()同时唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。

        使用示例

        使用条件变量对上文代码进行修改,当消费者线程没有产品可消费时,让它处于等待状态,知道生产者把产品生产出来;当生产者把产品生产出来之后,再去通知消费者。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>


static pthread_mutex_t mutex;
static pthread_cond_t cond;
static int g_avail = 0;

static void *consumer_thread(void *arg){
    for (;;) {
        pthread_mutex_lock(&mutex);         //互斥锁上锁
            pthread_cond_wait(&cond,&mutex);// 阻塞并等待条件满足
        while(g_avail > 0)
            g_avail--;                      //消费
        pthread_mutex_unlock(&mutex);       //互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[]){
    pthread_t tid;
    int ret;

    /* 初始化互斥锁和条件变量 */
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 生产 */
    for(;;){
        pthread_mutex_lock(&mutex);//互斥锁上锁
        g_avail++;              
        pthread_mutex_unlock(&mutex);//互斥锁解锁
        pthread_cond_signal(&cond);//向条件变量发送信号    
    }

    exit(0);
}

        全局变量 g_avail 作为主线程和新线程之间的共享资源,两个线程在访问它们之间首先会对互斥锁进行上锁,消费者线程中,当判断没有产品可被消费时(g_avail <= 0),调用 pthread_cond_wait()使得线程陷入等待状态,等待条件变量,等待生产者制造产品;调用 pthread_cond_wait()后线程阻塞并解锁互斥锁;而在生产者线程中,它的任务是生产产品(使用g_avail++来模拟),产品生产完成之后,调用pthread_mutex_unlock() 将互斥锁解锁,并调用 pthread_cond_signal()向条件变量发送信号;这将会唤醒处于等待该条件变量的消费者线程,唤醒之后再次自动获取互斥锁,然后再对产品进行消费(g_avai--模拟)。

条件变量的判断条件

        使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。譬如在示上节代码中,与条件变量相关的判断是(0 >= g_avail)。在这份示例代码中,我们使用了 while 循环、而不是 if 语句,来控制对 pthread_cond_wait()的调用,这是为何呢?

        必须使用 while 循环,而不是 if 语句,这是一种通用的设计原则:当线程从 pthread_cond_ wait()返回时, 并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。 从 pthread_cond_wait()返回后,并不能确定判断条件是真还是假,其理由如下:

  • 当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。譬如代码中, 如果有两个或更多个消费者线程,当其中一个消费者线程从 pthread_cond_wait()返回后,它会将全局共享变量 g_avail 的值变成 0,导致判断条件的状态由真变成假。
  • 可能会发出虚假的通知。

条件变量的属性

        如前所述,调用 pthread_cond_init()函数初始化条件变量时,可以设置条件变量的属性,通过参数 attr 指定。参数 attr 指向一个 pthread_condattr_t 类型对象,该对象对条件变量的属性进行定义,当然,如果将参数 attr 设置为 NULL,表示使用默认值来初始化条件变量属性。

        关于条件变量的属性本文不打算深入讨论,条件变量包括两个属性:进程共享属性和时钟属性。每个属性都提供了相应的 get 方法和 set 方法。

        

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值