线程同步—条件变量

条件变量

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

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

  在使用条件变量之前,必须先对它进行初始化。由 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章节

原文地址:https://www.cnblogs.com/yunfan1024/p/14140687.html