• 线程同步—条件变量


    条件变量

      互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程这一通知,在通知未到达之前,线程处于阻塞状态。条件变量本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

      条件变量总是结合互斥量使用的。条件变量就共享变量/临界资源的状态改变发出通知,而互斥量提供对该条件变量的互斥访问。

      在使用条件变量之前,必须先对它进行初始化。由 pthread_cond_t 数据类型表示的条件变量可以用两种方式进行初始化,可以把常量 PTHREAD_COND_INITIALIZER 赋给静态分配的条件变量,但是如果条件变量是动态分配的,则需要使用pthread_cond_init() 函数对它进行初始化。

      在释放条件变量底层的内存空间之前,可以使用 pthread_cond_destroy() 函数对条件变量进行反初始化(deinitialize)。

    1 include <pthread.h>
    2 
    3 int pthread_cond_init(pthread_cond_t *restrict cond,
    4                const pthread_condattr_t *restrict attr);
    5 
    6 int pthread_cond_destroy(pthread_cond_t *cond);
    7  
    8 //条件变量的静态初始化方式
    9 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    • 两个函数的返回值:若成功,返回0;否则,返回错误编号。

      除非需要创建一个具有非默认属性的条件变量,否则 pthread_cond_init() 函数的 attr 参数可以设置为 NULL。

    条件变量的操作

      条件变量的操作主要是发送通知信号(Signal)和等待(Wait)。发送信号操作即通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变。等待操作是指在收到一个通知前一直处于阻塞状态。

    1. 设置等待条件

      我们使用 pthread_cond_wait() 函数等待条件变量为真。如果在给定的时间内条件不能满足,那么会返回一个错误编号。

    1 #include <pthread.h>
    2 
    3 int pthread_cond_wait(pthread_cond_t *restrict cond,
    4       pthread_mutex_t *restrict mutex);
    5 
    6 int pthread_cond_timedwait(pthread_cond_t *restrict cond,
    7       pthread_mutex_t *restrict mutex,
    8       const struct timespec *restrict abstime);
    • 两个函数的返回值:若成功,返回0;否则,返回错误编号。

    《函数说明》

    (1)传递给 pthread_cond_wait() 函数的互斥量对条件变量进行保护。调用者把锁住的互斥量传给该函数,函数自动把调用线程放到等待条件的线程列表中,然后在 pthread_cond_wait() 函数内部对互斥量解锁。在未接收到条件变量状态改变的通知之前,当前线程会阻塞在 pthread_cond_wait() 函数中;一旦接收到状态改变的通知“信号”,pthread_cond_wait() 才会返回,并且在该函数内部互斥量会被再次锁定,因此,该函数返回后,还需要对互斥量进行一次解锁操作。

    (2)pthread_cond_timedwait() 函数的功能与 pthread_cond_wait() 函数类似,只是多了一个超时时间。超时值指定了我们愿意等待多长时间,它是通过 timespec 结构体指定的。这个时间值是一个绝对数而不是一个相对数。例如,假设愿意等待3分钟,那么,并不是把3分钟转换成 timespec 结构,而是需要把当前时间加上3分钟再转换成 timespec 结构。可以使用 clock_gettime() 函数获取 timespec 结构体表示的当前时间,但是并不是所有的平台都支持这个函数,Linux系统是支持的,clock_gettime() 函数是在librt库中实现的,所以需要加上-lrt库链接。当然,也可以使用另一个函数 gettimeofday() 获取 timeval 结构表示的当前时间,然后把这个时间转换成 timespec 结构体。要得到超时值的绝对时间,可以使用下面的函数(假设阻塞的最大时间使用分钟来表示):

    #include <stdlib.h>
    #include <sys/time.h>
    
    void maketimeout(struct timespec *tsp, long minutes)
    {
       struct timeval now;
        
       //get the current time
        gettimeofday(&now, NULL);
      tsp->tv_sec = now.tv_sec;
      tsp->tv_nsec = now.tv_usec * 1000;  //usec(微秒)-->nsec(纳秒)
      //add the offset to get timeout value
        tsp->tv_sec += minutes * 60;
    }

     <链接> struct timespec 和 struct timeval 结构体定义
      如果超时时间到期后,条件还是没有出现,pthread_cond_timedwait() 将重新获取互斥量,然后返回错误 ETIMEDOUT。从 pthread_cond_wait 或者 pthread_cond_timedwait 调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件。

    2. 设置通知条件

      有两个函数可以用于通知线程条件已经满足。pthread_cond_signal() 函数至少能唤醒一个等待该条件的线程,而 pthread_cond_broadcast() 函数则能唤醒等待该条件的所有线程。

    #include <pthread.h>
    
    int pthread_cond_signal(pthread_cond_t *cond);
    int pthread_cond_broadcast(pthread_cond_t *cond);
    • 两个函数的返回值:若成功,返回0;否则,返回错误编号。

      在调用 pthread_cond_signal 或 pthread_cond_broadcast 时,我们说这是给线程或者条件发送通知信号(Signal)。必须注意的是,一定要在改变状态以后再给线程发通知信息。

    <备注> POXIS 规范为了简化 pthread_cond_signal 的实现,允许它在实现的时候唤醒一个以上的线程。

    示例:在生产者-消费者模型中,结合使用条件变量和互斥量对线程进行同步。代码如下:prod_condvar.c

    #include <stdio.h>
    #include <stdbool.h>
    #include <unistd.h>
    #include <time.h>
    #include <pthread.h>
    
    //对静态互斥量的初始化
    static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
    //对静态条件变量的初始化
    static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    
    static int avail = 0;  //共享变量,记录已生产可供消息的产品数量
    
    static void *
    producer(void *arg)
    {
        int cnt = atoi((char*)arg);
        int ret, i;
        
        printf("producer: pid=%lu, tid=%lu
    ", getpid(), pthread_self());
        for(i=0; i<cnt; i++){
            sleep(1);
            ret = pthread_mutex_lock(&mtx);  //对共享变量 avail 需要互斥访问
            if(ret != 0)
                printf("pthread_mutex_lock failed!
    ");
            avail++;  //生成一个产品
            ret = pthread_mutex_unlock(&mtx);
            if(ret != 0)
                printf("pthread_mutex_unlock failed!
    ");
            ret = pthread_cond_signal(&cond);  //唤醒消费者
            if(ret != 0)
                printf("pthread_cond_signal failed!
    ");
        }
        return NULL;
    }
    
    int
    main(int argc, char *argv[])
    {
        pthread_t tid;
        int ret, i;
        int totRequired;  //所有线程将要生产的产品的总数
        int numConsumed;  //消费者已消费的产品数
        
        bool done;     //商品是否消费完成标志
        time_t t;
        
        t = time(NULL);
        
        printf("main: pid=%lu, tid=%lu
    ", getpid(), pthread_self());
        //创建所有线程
        totRequired = 0;
        for(i=1; i<argc; i++){
            totRequired += atoi(argv[i]);
            ret = pthread_create(&tid, NULL, producer, argv[i]);
            if(ret != 0){
                printf("pthread_create failed!
    ");
            }
        }
        
        //消费者循环消费已生产出来的产品
        numConsumed = 0;
        done = false;
        for(;;){
            pthread_mutex_lock(&mtx);
            if(avail == 0){
                ret = pthread_cond_wait(&cond, &mtx);  //等待唤醒通知
                if(ret != 0)
                    printf("pthread_cond_wait failed!
    ");
            }
            //程序运行到这里时,互斥量仍是lock的
            while(avail > 0){
                numConsumed ++;  //消费者已消费商品数加1
                avail --;        //现存商品数减1
                printf("T=%ld, numConsumed=%d
    ", (long)(time(NULL)-t), numConsumed);
                done = numConsumed >= totRequired;  //当所有生产的商品都已消费完成,done置为true
            }
            pthread_mutex_unlock(&mtx);
            
            if(done)
                break;
        }
        
        return 0;
    }

    **编译命令: gcc prod_condvar.c -o prod_condvar -lpthread

    **运行命令: ./prod_condvar 4 5 6

    **运行结果:

    main: pid=20056, tid=140332428621632
    producer: pid=20056, tid=140332420314880
    producer: pid=20056, tid=140332411922176
    producer: pid=20056, tid=140332403529472
    T=1, numConsumed=1
    T=1, numConsumed=2
    T=1, numConsumed=3
    T=2, numConsumed=4
    T=2, numConsumed=5
    T=2, numConsumed=6
    T=3, numConsumed=7
    T=3, numConsumed=8
    T=3, numConsumed=9
    T=4, numConsumed=10
    T=4, numConsumed=11
    T=4, numConsumed=12
    T=5, numConsumed=13
    T=5, numConsumed=14
    T=6, numConsumed=15
    

     《代码分析》

    • 运行命令:./prod_condvar 4 5 6,表示的含义是生产者线程1、2、3生产的产品个数分别是4、5、6,共计15个。从运行结果可以看到,本示例中,共有4个线程,其中主线程是main函数,亦即消费者线程,而其他三个线程是生产者线程producer,也就是说总共有3个生产者,1个消费者。这四个线程同时共享全局变量 avail。
    • 生产者负责生产商品,当生产者每生产出1个商品,共享变量avail自增加1,然后使用 pthread_cond_signal 唤醒main函数中的消费者线程,通知其可以消费商品了。而对于消费者线程,刚开始的时候,avail == 0,因此使用 pthread_cond_wait 设置等待条件,此时消费者线程会处于阻塞状态,直到接收到生产者 producer 发出的唤醒通知,消费者线程开始继续执行,并开始消费已生产出来的商品。
    • 当消费者已消费的商品数 >= 所有生产者生产出来的商品时,退出 for循环,结束主线程,同时整个进程结束。

    参考

    《UNIX环境高级编程(第3版)》第11.6.6章节

    《Linux_Unix系统编程手册(上)》第30.2章节

  • 相关阅读:
    学点 C 语言(40): 函数 多参函数
    存取 ListBox 列表 回复 "徐强" 的问题
    博客园RSS订阅汇总
    博客园电子期刊2012年2月刊发布啦
    上周热点回顾(3.53.11)
    博客园电子期刊2012年3月刊发布啦
    上周热点回顾(3.264.1)
    上周热点回顾(3.193.25)
    上周热点回顾(4.24.8)
    上周热点回顾(2.273.4)
  • 原文地址:https://www.cnblogs.com/yunfan1024/p/14140687.html
Copyright © 2020-2023  润新知