linux 应用层同步与互斥机制之条件变量

2、条件变量

互斥量防止多个线程同时访问同一共享变量。(我们称为互斥)

有一种情况,多个线程协同工作。一个线程的消费需要等待另一个线程的产出。必须线程B完成了应有的任务,满足了某一个条件,线程A才能继续执行。(我们称为同步)

条件变量就是来解决同步问题的。

2.1 条件变量产生背景

用一个典型的例子(生产-消费)说明:

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static int avail = 0;

/*生产者线程示意代码*/

s = pthread_mutex_lock(&mtx);

if(s != 0)

    do_err();

avail++;

s = pthread_mutex_unlock(&mtx);

if(s != 0)

    do_err();

/*消费者线程示意代码*/

for(;;){

    s = pthread_mutex_lock(&mtx);

    if(s != 0)

        do_err();

   

    while(avail > 0)

        avail--;

        /*do something*/

    }

   

    s = pthread_mutex_unlock(&mtx);

    if(s != 0)

        do_err();

   

}

   

上述代码,生产者线程在满足一定条件下,将avail++。消费者线程不停的循环检查变量avail的状态,一旦有可用资源,就进行消费处理。虽然可行,但循环检查会造成CPU的资源的浪费。条件变量就是为解决这一问题而设计:允许一个线程休眠(等待)直至接获另一线程的通知(收到信号)去执行某些操作。

2.2 条件变量初始化和销毁

条件变量的数据类型是pthread_cond_t。

静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER

动态初始化:pthread_cond_init

#include <pthread.h>

int

pthread_cond_init(pthread_mutex_t *restrict cond, const pthread_condattr_t *restrict attr);

                                                                                                                                            成功:0 失败:非0

涉及动态初始化的变量,就要有销毁

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *restrict mutex);                                                                                                                                          成功:0 失败:非0

条件变量销毁:pthread_cond_destroy

条件变量的初始化和销毁的注意事项,类似于互斥量。

2.3 条件变量的通知和等待

2.3.1 函数定义和基本用法

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

#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

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

                                                                                                                                            成功:0 失败:非0

看一下手册上的解释:

The pthread_cond_wait() functions shall block on a condition variable。

The pthread_cond_signal() function shall unblock at least one of the threads that are blocked on the specified condition variable cond (if any threads are blocked on cond).

The pthread_cond_broadcast() function shall unblock all threads currently blocked on the specified condition variable cond.

在解释具体参数前,我们先利用这些新函数,优化一下上面的“生产-消费”例子,看一下基本用法。

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static int avail = 0;

/*生产者线程示意代码*/

s = pthread_mutex_lock(&mtx);

if(s != 0)

    do_err();

avail++;

s = pthread_cond_signal(&cond);

if(s != 0)

    do_err();

s = pthread_mutex_unlock(&mtx);

if(s != 0)

    do_err();

/*消费者线程示意代码*/

for(;;){

    s = pthread_mutex_lock(&mtx);

    if(s != 0)

        do_err();

   

    while(avail == 0){   //注意,这里不能用if,用while

        s = pthread_cond_wait(&cond, &mtx);

        if(s != 0)

            do_err();

    }

    while(avail > 0)

        avail--;

        /*do something*/

    }

   

    s = pthread_mutex_unlock(&mtx);

    if(s != 0)

        do_err();

   

}

   

2.3.2 pthread_cond_wait函数用法

条件变量与互斥量的天然联系

pthread_cond_wait内部执行的操作如下:

  1. 解锁互斥量mutex
  2. 阻塞调用线程,直至另一个线程就条件变量cond发出信号
  3. 重新锁定mutex

所以,条件变量总是要与一个互斥量相关。大家自然也就明白了pthread_cond_wait的第二个参数的意义。pthread_cond_wait必须在pthread_mutex_lock和pthread_mutex_unlock之间。等待相同条件变量的所有线程在调用pthread_cond_wait时必须指定同一互斥量。

pthread_cond_wait中,解锁互斥量和陷入对条件变量的等待属于一个原子操作。调用该函数时,在调用线程陷入对条件变量的等待之前,其他线程不可能获取到该互斥量,也不可能就该条件变量发出信号。

pthread_cond_wait使用的通用原则

从上面“生产-消费”的例子中,可以看到ptread_cond_wait函数调用放在了一个while循环中,而不是用if来判断,这是使用条件变量等待条件触发时的一个通用的设计原则。当代码从pthread_cond_wait()返回时,并不能确定判断条件的状态,应该立即重新检查判断条件,在条件不满足的情况下继续休眠等待。

最典型情况时存在多个消费线程等待条件变量通知。如果生产线程调用pthread_cond_broadcast()来唤醒多个等待的消费线程,那么只能有一个消费线程能够获取资源,进入下一步处理,其他消费线程没有竞争到可用资源,只能继续wait。

思考一下:

如果生产线程调用pthread_cond_signal()来唤醒一个等待的消费线程,上面的情况还会出现吗?

在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果就是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()pthread_cond_timedwait()的线程返回。这种效应就称为“虚假唤醒”。

所以pthread_cond_signal()手册中的说明是至少唤醒一个等待线程

不论是使用while还是if,都是引入了一个共享变量,来标识是否有可用资源。这里扩展一下,说明两个概念:边沿触发和水平触发。比如消费者代码如下写法:

/*消费者线程示意代码*/

for(;;){

    s = pthread_mutex_lock(&mtx);

    if(s != 0)

        do_err();

   

    s = pthread_cond_wait(&cond, &mtx);

    if(s != 0)

        do_err();

   

    while(avail > 0)

        avail--;

        /*do something*/

    }

   

    s = pthread_mutex_unlock(&mtx);

    if(s != 0)

        do_err();

   

}

调用pthread_cond_wait时,不加任何条件判断,直接就等着。会发生什么?

因为时多线程,生产者线程可能先运行,即:有可能在调用pthread_cond_wait前,生产者线程已经调用了pthread_cond_signal()。pthread_cond_signal就是发个信号,唤醒一个在等待的线程。如果没有在等待的,就这样了。这种不保留通知事件的情况,就是边沿触发。要求关心事件的线程必须提前做好准备。所以上面的写法,就有可能丢失事件。

当我们加入一个共享变量,作为判断条件时,这个变量实际起到了记录事件的作用,将事件的有效期延长了。这就是水平触发。编程水平触发后,消费者进入wait前,先判断是否有事件发生,这样就不会丢失事件。

2.3.3 pthread_cond_signal函数用法

这个函数的使用比较简单,调用pthread_cond_signal函数时,不一定非得使用mutex互斥量。

思考一个问题:当使用mutex互斥量时,调用pthread_cond_signal()函数发送信号的时机。是放在pthread_mutex_unlock之前还是之后?

之前:

pthread_mutex_lock

    xxxxxxx

pthread_cond_signal

pthread_mutex_unlock

缺点:在某些系统的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为)。如:线程A调用signal唤醒线程B后,还没有来得及调用unlock,就切换了。后来线程B先运行了,线程B被唤醒,准备进行lock操作,发现mutex还被占用,进入阻塞。这中间可能涉及内核层和用户层切换问题。所以一来一回会有性能损耗。

但是在LinuxThreads里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。

所以在Linux中推荐使用这种模式。

之后:

pthread_mutex_lock

    xxxxxxx

pthread_mutex_unlock

pthread_cond_signal

优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了

缺点:有可能在unlock之后,signal之前就被调度了。如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),因为wait的那个线程在等cond,没有在等mutex。而这在上面的放中间的模式下是不会出现的。

所以,在Linux下最好pthread_cond_signal放中间,但从编程规则上说,两种都可以。

2.4 条件变量适用场景

类似于Mutex,条件变量也是可以用于同一进程的线程之间,也可以用于跨进程。

但不建议使用跨进程,因为比较复杂,除了设置条件变量的跨进程属性外,mutex也要跨进程。

有一篇资料提出,慎用进程间条件变量:

条件变量是用于多线程/多进程间同步,是一种典型的睡眠唤醒用法。P1等待某个事件的发生,P2触发事件,唤醒P1

条件变量在初始化时,可以通过接口pthread_condattr_setpshared指定该条件变量可用于进程内的线程间同步,还是用于进程间同步。

但是,在linuxglibc实现中,进程间同步却存在着一个缺陷。将导致问题扩散,非常严重。原因如下:

pthread_cond_t结构体是一个复杂的数据结构,包含了等待信息,多个进程都可能同时调用等待函数pthread_cond_wait/pthread_cond_timedwait,唤醒函数pthread_cond_signal/pthread_cond_broadcast,为了防止一个或者多个等待者、唤醒者同时操作pthread_cond_t的成员,必须进行互斥,所以,pthread_cond_t里还有一个锁。接口实现上,pthread_cond_wait/pthread_cond_timedwaitpthread_cond_signal/pthread_cond_broadcast都必须先获取锁,然后操作数据,完毕释放锁。

在多进程上,如果某个进程在pthread_cond_xxx的接口里获取了锁以后,因某种原因退出了(比如某个线程运行异常了)。那么,悲剧来了,锁没有释放。于是,其他的进程只要调用到这个条件变量的接口,将因为获取不到锁而等待,且一直等待下去。一个进程的异常导致所有进程的异常。

令人困惑的是,mutex也可用于进程间互斥,pthread_mutex_setpshared设置。但是pthread_mutex_t却支持这种场景,进程获取到了mutex后复位了,没有释放锁,OS帮忙释放(需要在mutex初始化时设置pthread_mutexattr_setrobust_np)。

同样可用于进程间的条件变量为什么没有这个机制?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一叶知秋yyds

分享是一种美德,感谢金主打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值