Linux进/线程同步方式2——条件变量

一、背景概述

互斥锁是线程程序必须的工具,但并非是万能的。例如,如果一个线程正在等待共享数据内某个条件出现,那会发生什么呢?它可能重复地对互斥对象进行锁定和解锁,每次都会检查共享数据结构,以查找某个值是否满足要求。但这是在浪费时间和资源,而且这种繁忙查询的效率非常低。

在每次检查之间,可以让调用线程短暂地进入睡眠,比如睡眠3秒,但是由此线程代码就无法最快做出响应。那是否有这样的一种方式呢:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就唤醒因等待满足特定条件而睡眠的线程。如果能够做到这一点,线程代码将会变得非常高效,并且不会占用宝贵的互斥锁以及CPU资源。因此条件变量就应用而生了。

为了说明问题,来看一个没有使用条件变量的例子,生产者——消费者模式。生产者负责生产产品,而消费者负责消费产品。对于消费者来说,没有产品的时候只能等待产品被生产出来,一旦有产品就使用它。
这里我们使用一个变量来表示这种产品,生产者生产一件产品变量就加+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--;              //消费
            fprintf(stdout, "cousume a product, remaining: %d\n", g_avail);
        }
        pthread_mutex_unlock(&mutex);//互斥锁解锁
        sleep(2);
    }
    return (void *)0;
}

static void *productor_thread(void *arg){
    for (;;) {
        pthread_mutex_lock(&mutex);//互斥锁上锁
        g_avail++;              //生产
        fprintf(stdout, "produce a product, remaining: %d\n", g_avail);
        pthread_mutex_unlock(&mutex);//互斥锁解锁
        sleep(1);
    }
    return (void *)0;
}
 
int main(int argc, char *argv[]){
    int ret = 0;
    pthread_t tid1;
    pthread_t tid2;
 
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    
    /* 创建新线程 */
    ret = pthread_create(&tid1, NULL, productor_thread, NULL);    //生产者线程
    ret |= pthread_create(&tid2, NULL, consumer_thread, NULL);   //消费者线程
    if (0 != ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        return -1;
    }
    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    ret |= pthread_join(tid2, NULL);
    if (0 != ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        return -1;
    }

    return 0;
}

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

二、条件变量运行流程

在这里插入图片描述

三、初始化和销毁条件变量

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

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态初始化和销毁函数原型如下:

#include <pthread.h>

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

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

参数cond指向pthread_cond_t条件变量对象,对于pthread_cond_init()函数,类似于互斥锁,在初始化条件变量时设置条件变量的属性。参数attr指向一个pthread_condattr_t类型的对象。一般将参数attr设置为NULL,表示使用属性的默认值来初始化条件变量,与使用PTHREAD_COND_INITIALIZER宏相同。当然也可以设置为进程共享属性和时钟属性。具体使用方法可以待使用时再查找。
函数调用成功返回0,失败将返回一个非0值的错误码。

对于初始化和销毁条件变量操作,有以下问题需要注意:

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

四、通知和等待条件变量

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

#include <pthread.h>

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

参数cond指向目标条件变量,向该条件变量发送信号。调用成功返回0,失败则返回非0值的错误码。
pthread_cond_signal()和pthread_cond_broadcast()的区别在于:二者对阻塞于条件变量上的多个线程对应的处理方式不同,pthread_cond_signal()函数至少能唤醒一个线程,而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: 指向一个互斥锁对象。条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的,因为可能存在多个线程在条件变量上等待,因此等待队列是所有线程共享的资源)是在互斥锁的保护下进行的,也就是说条件变量本身是由互斥锁保护的。
返回值: 调用成功返回0,失败将返回一个非0值的错误码。在pthread_cond_wait()函数内部会对参数mutex所指定的互斥锁进行操作,通常情况下,条件判断以及pthread_cond_wait()函数调用均在互斥锁的保护下,也就是说,在此之前线程已经对互斥锁加锁了。调用pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程添加到等待条件的线程列表上,然后将互斥锁解锁,当pthread_cond_wait()被唤醒返回时,其内部会再次锁住互斥锁。

pthread_cond_timedwait()函数原型如下:

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

该函数为计时等待,如果在设定时间内条件变量没有触发就返回错误。调用成功返回0,出错返回错误码。
abstime指向一个timespec结构体,该结构体如下所示:

struct timespec
{
	time_t tv_sec;   //seconds
	long tv_nsex;    //nanoseconds纳秒
}

如果在abstime指定的时间内cond未触发,互斥锁mutex被重新加锁,并返回错误ETIMEDOUT,结束等待。abstime和time()系统调用函数一样都是绝对时间。0就表示格林尼治时间1970年1月1日0时0分0秒。

五、应用举例

生产者——消费者模型

#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);         //互斥锁上锁
        /*
        这里必须使用while而不能是if。因为可能条件满足时当前的线程获取到互斥锁时,
        g_avail已经被别的线程抢先消费掉了,此时条件又不满足了。所以线程被唤醒以后
        也必须再经过条件判断,符合条件才跳出while继续往下执行。
        */
        while(0 == g_avail)
        {
            pthread_cond_wait(&cond, &mutex);   //阻塞并等待条件满足
        }
        g_avail--;         //消费
        printf("消费g_avail:%d\n", g_avail);
        pthread_mutex_unlock(&mutex);       //互斥锁解锁
    }
    return (void *)0;
}
 
int main(int argc, char *argv[]){
    int ret = 0;
    pthread_t tid;
 
    /* 初始化互斥锁和条件变量 */
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (0 != ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
 
    /* 生产 */
    for(;;){
        pthread_mutex_lock(&mutex);//互斥锁上锁
        g_avail++;
        printf("生产g_avail:%d\n", g_avail);        
        pthread_mutex_unlock(&mutex);//互斥锁解锁
        pthread_cond_signal(&cond);//向条件变量发送信号    
    }
 
    exit(0);
}

六、问题答疑

  1. 为什么pthread_cond_wait需要加锁?
  • pthread_cond_wait中的mutex用于保护条件变量,调用这个函数进行等待条件的发生时,mutex会自动释放,以供其他线程(生产者)改变条件,pthread_cond_wait中的两个步骤必须是原子性的,也就是说必须把两个步骤捆绑在一起:
    • 把调用线程添加到条件等待队列上;
    • 释放mutex;
  • 不然呢,如果不是原子性的,上面的两个步骤之间就可能插入其他操作。比如,如果先释放mutex,这时生产者线程改变条件状态,然后signal,之后消费者线程才去把当前调用线程添加到等待队列上,这样的话signal信号就被丢了。
  • 如果先把调用者线程添加到条件等待队列上,这时另外一个线程发送了signal信号,此时调用者线程并没有释放mutex,由于收到了signal信号,其会立即获取mutex。两次加锁就导致了死锁。
  1. 消费者线程中判断条件由while换成if可不可以呢?
    不可以。
  • 一个生产者可能对应着多个消费者,生产者改变等待条件的状态后发出signal,然后各个消费者线程的pthread_cond_wait获取mutex后返回,当然,这里只有一个线程获取到了mutex,然后进行处理,其他线程会pending在这里,处理线程处理完毕之后释放mutex,刚才等待的线程中有一个获取mutex,如果这里用if,就会在当前队列为空的状态下继续往下处理,这显然是不合理的。
  • 再具体点,有可能多个线程都在等待这个资源可用的信号,信号发出后只有一个资源可用,但是有A、B两个线程都在等待,B速度比较快,获得互斥锁,然后加锁,消耗资源,然后解锁,之后A获得互斥锁,但A回去发现资源已经被使用了,它便有两个选择,一个是去访问不存在的资源,另一个就是继续等待,那么继等待下去的条件就是使用while,要不然使用id的话pthread_cond_wait返回后,就会顺序执行下去。
  1. signal到底是放在unlock之前还是之后?
void enqueue_msg(struct msg *mp)
{
	pthread_mutex_lock(&qlock);
	mp->m_next = workq;
	workq = mp;
	pthread_mutex_unlock(&qlock);
	pthread_cond_signal(&qready);
}

如果先unlock,再signal。如果这时候有一个消费者线程恰好获取mutex,然后进入条件判断,这里就会判断成功,从而跳过pthread_cond_wait,下面的signal就会不起作用;另外一种情况,一个优先级更低的不需要条件判断的线程正好也需要这个mutex,这时就会转去执行这个优先级低的线程,就违背了设计的初衷。

 void enqueue_msg(struct msg *mp)
{
	pthread_mutex_lock(&qlock);
	mp->m_next = workq;
	workq = mp;
	pthread_cond_signal(&qready);
	pthread_mutex_unlock(&qlock);
}

如果把signal放在unlock之前,消费者线程会被唤醒,获取mutex发现获取不到,就又去sleep了,浪费了资源。但是在LinuxThreads或者NPTL里面,就不会有这个问题。一位内在Linux线程中,有两个队列,分别是cond_wait队列和mutex_lock队列,cond_signal只是让线程从cond_wait队列移动到mutex_lock队列,而不用返回到用户空间,因此不会有性能上的损耗。这样的话即使消费者线程又sleep了以后,只要它被移动到mutex_lock队列就会再次立即去获取mutex锁。

七、利用条件变量实现进程间同步示例

利用共享内存存放条件变量和互斥锁来达到同步的目的。
GetCondWait.c

#include "public.h"

char* get_cond_wait(test_cond_mutex** cond_mutex, char* process_name)
{
    int fd = 0;
    int ret = 0;
    char* g_share_mem;
    test_cond_mutex* cond_mutex_t;
    if(!strcmp(process_name, "CondSignal") )
    {
        shm_unlink("/test_share_mem"); 
    }

    fd = shm_open("/test_share_mem", O_RDWR|O_CREAT|O_EXCL, 0777);
    if(fd > 0)
    {
        // create ok,设置共享内存长度
        (void)ftruncate(fd, sizeof(test_cond_mutex));
        g_share_mem = (char*)mmap(NULL, sizeof(test_cond_mutex), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

        memset(g_share_mem, 0, sizeof(test_cond_mutex));

        cond_mutex_t = (test_cond_mutex*)g_share_mem;

        // 初始化一个进程间同步的等待变量
        pthread_condattr_t cond_attr;
        ret = pthread_condattr_init(&cond_attr);
        ret |= pthread_condattr_setpshared(&cond_attr, PTHREAD_PROCESS_SHARED);
        ret |= pthread_cond_init(&cond_mutex_t->cond, &cond_attr);
        if (0 != ret)
        {
            printf("条件变量错误");
            // return -1;
        }
        


        // 初始化一个可以在进程间同步使用的锁
        pthread_mutexattr_t mutex_attr;
        ret = pthread_mutexattr_init(&mutex_attr);
        ret |= pthread_mutexattr_setpshared(&mutex_attr, PTHREAD_PROCESS_SHARED);
        ret |= pthread_mutex_init(&cond_mutex_t->mutex, &mutex_attr);
        if (0 != ret)
        {
            printf("互斥锁错误");
            // return -1;
        }
    }
    else
    {
        fd = shm_open("/test_share_mem", O_RDWR, 0777);
        // 其他进程已经初始化过了,这里不要再初始化
        g_share_mem = (char*)mmap(NULL, sizeof(test_cond_mutex), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

        cond_mutex_t = (test_cond_mutex*)g_share_mem;
    }
    // 所有对该文件的映射都unmap之后,该文件会真正的删除,
    // 保证该文件在所有通信的进程退出后,会被删除
    close(fd);
    //    shm_unlink("/test_share_mem"); 
    *cond_mutex = cond_mutex_t;

    return g_share_mem;
}

CondWait.c

#include "public.h"

extern char* get_cond_wait(test_cond_mutex** cond_mutex, char* process_name);
test_cond_mutex* cond_mutex;
int main()
{
    char* g_share_mem;

    g_share_mem = get_cond_wait(&cond_mutex, "CondWait");

    while(1)
    {
        pthread_mutex_lock(&cond_mutex->mutex); 
        while (0 == cond_mutex->num)
        {
            pthread_cond_wait(&cond_mutex->cond, &cond_mutex->mutex); 
        }
        printf("wait ok:%d\n", cond_mutex->num);
        cond_mutex->num = 0;
        pthread_mutex_unlock(&cond_mutex->mutex);
    }


    return 0;
}

CondSignal.c

#include "public.h"

extern char* get_cond_wait(test_cond_mutex** cond_mutex, char* process_name);

int main()
{
    int num = 0;
    char* g_share_mem;
    test_cond_mutex* cond_mutex;

    g_share_mem = get_cond_wait(&cond_mutex, "CondSignal");
    
    while(1)
    {
        num = 0;
        while (num <= 0)
        {
            printf("Please enter an integer greater than 0:");
            scanf("%d", &num);
            if(num <= 0)
            {
                printf("input error, please re-enter!");
            }
        }
        
        pthread_mutex_lock(&cond_mutex->mutex);
        cond_mutex->num = num;
        pthread_cond_signal(&cond_mutex->cond);
        pthread_mutex_unlock(&cond_mutex->mutex);
    }
}

public.h

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>

typedef struct test_cond_mutex
{
    unsigned int num;
    pthread_cond_t cond;
    pthread_mutex_t mutex;
}test_cond_mutex;

Makefile

all: cond_wait cond_signal

cond_wait: CondWait.c GetCondWait.c
	gcc -o cond_wait CondWait.c GetCondWait.c  -lpthread -lrt

cond_signal: CondSignal.c GetCondWait.c
	gcc -o cond_signal CondSignal.c GetCondWait.c -lpthread -lrt

八、番外篇之按照相对时间条件变量等待超时示例

pthread_cond_timedwait()在官方文档中介绍了按绝对时间等待超时,但是几乎没有对按相对等待做说明。然而绝对时间模式有个致命的缺点,就是在设置等待时间后,若系统时间发生了调整,可能出现永远等不到超时的极端情况。使用相对时间可以避免上述问题。
下面列出一段示例代码说明如何使得pthread_cond_timedwait按相对时间等待超时,其中最关键的一点就是使用pthread_condattr_setclock设置pthread_cond_timedwait成相对时间模式。

#include <stdio.h>
#include <time.h>
#include <pthread.h>
 
typedef struct mutex_cond
{
		pthread_condattr_t cattr;
        pthread_mutex_t i_mutex;
        pthread_cond_t i_cv;
		void* i_sigevent; 
}mutex_cond_t;
 

int main()
{
	mutex_cond_t mcond;
	int ret = pthread_condattr_init(&(mcond.cattr));
	if (ret != 0)
	{
		return (-1);
	}
	mcond.i_sigevent = NULL;
    ret = pthread_mutex_init ( &(mcond.i_mutex), NULL);
	ret = pthread_condattr_setclock(&(mcond.cattr), CLOCK_MONOTONIC);
	ret = pthread_cond_init(&(mcond.i_cv), &(mcond.cattr));
 
	struct timespec tv;
	while(1)
	{
		clock_gettime(CLOCK_MONOTONIC, &tv);
		printf("now time:%d\n", tv.tv_sec);
		tv.tv_sec += 20;// 设置20秒后没收到事件超时返回
        pthread_mutex_lock(&mcond.i_mutex);
		ret = pthread_cond_timedwait(&(mcond.i_cv), &(mcond.i_mutex), &tv);
        pthread_mutex_unlock(&mcond.i_mutex);
    }
	
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值