Unix/Linux编程:条件变量

互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(其他共享资源)的状态变化通知其他线程,并让其他线程等待(堵塞于)这一通知。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • 一个线程等待"条件变量的条件成立"而挂起;
  • 另一个线程使"条件成立"(给出条件成立信号)。

为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

初始化

用静态分配的条件变量

条件变量的数据类型是 pthread_cond_t 。类似于互斥量,使用条件变量前必须对其初始化。对于经由静态分配的条件变量,将其赋值为 PTHREAD_COND_INITALIZER 即完成初始化操作。可参考下面的例子:

pthread_cond_t cond = PTHREAD_COND_INITALIZER 

动态分配的条件变量

使用函数 pthread_cond_init()对条件变量进行动态初始化。需要使用 pthread_cond_init()的情形类似于使用 pthread_mutex_init()来动态初始化互斥量的情况。亦即,对自动或动态分配的条件变量进行初始化时,或是对未采用默认属性经由静态分配的条件变量进行初始化时,必须使用 pthread_cond_init()。

NAME
       pthread_cond_destroy, pthread_cond_init - destroy and initialize condition variables

SYNOPSIS
       #include <pthread.h>

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

SUSv3 规定,对业已初始化的条件变量进行再次初始化,将导致未定义的行为。应当避免这一做法

当不再需要一个经由自动或动态分配的条件变量时,应调用 pthread_cond_destroy()函数予以销毁。对于使用 PTHREAD_COND_INITIALIZER 进行静态初始化的条件变量,无需调用pthread_cond_destroy()。

经 pthread_cond_destroy()销毁的条件变量,之后可以调用 pthread_cond_init()对其进行重新初始化

通知和等待条件变量

条件变量的主要操作是发送信号(signal)和等待(wait):

  • 发送新操作即通知一个或者多个出于等待状态的信号,某个共享变量的状态已经改变
  • 等待操作是指在收到一个通知前一直处于阻塞状态

函数 pthread_cond_signal()和 pthread_cond_broadcast()均可针对由参数 cond 所指定的条件变量而发送信号。pthread_cond_wait()函数将阻塞一线程,直至收到条件变量 cond 的通知。

NAME
       pthread_cond_broadcast, pthread_cond_signal - broadcast or signal a condition
       pthread_cond_timedwait - wait on a condition

SYNOPSIS
       #include <pthread.h>

       int pthread_cond_broadcast(pthread_cond_t *cond);
       int pthread_cond_signal(pthread_cond_t *cond);
	   int pthread_cond_wait(pthread_cond_t   *cond,   pthread_mutex_t   *mutex)  

函数 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_signal()会比 pthread_cond_broadcast()更具效率,因为这可以避免发生如下情况。

  • 同时唤醒所有等待线程
  • 某一线程首先获得调度。此线程检查了共享变量的状态(在互斥量的保护下),发现还有任务需要完成。该线程执行了所需工作,并改变共享状态,以表明任务完成,最后释放对相关互斥量的锁定
  • 剩余的每个线程轮流锁定互斥量并检测共享变量的状态。不过,由于第一个线程所做的工作,余下的线程发现无事可做,随即解锁互斥量转而休眠(即再次调用 pthread_cond_wait())。

相形之下,函数 pthread_cond_broadcast()所处理的情况是:处于等待状态的所有线程执行的任务不同(即各线程关联于条件变量的判定条件不同)

ps,所谓的虚假唤醒,形象的意思就是你妈同时通知你爸和你去做饭,做饭只需要一个人做即可, 当你去准备做饭时,你爸已经在做饭了,所以你就相当于被虚假唤醒了。

条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。发送信号时如果没有任何线程在等待该条件变量,这个信号也会不了了之。线程如果在此后等待该条件变量,只有当收到此变量的下一信号时,方可接触阻塞状态。

函数 pthread_cond_timedwait()与函数 pthread_cond_wait()几近相同,唯一的区别在于,由参数 abstime 来指定一个线程等待条件变量通知时休眠时间的上限

NAME
       pthread_cond_timedwait- wait on a condition

SYNOPSIS
       #include <pthread.h>

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

参数 abstime 是一个 timespec 类型的结构,用以指定自 Epoch以来以秒和纳秒(nanosecond)为单位表示的绝对(absolute)时间。如果 abstime 指定的时间间隔到期且无相关条件变量的通知,则返回 ETIMEOUT 错误

条件变量总是要与一个互斥量相关。将这些对象通过函数参数传递给 pthread_cond_wait(),后者执行如下操作步骤

  • 解锁互斥量 mutex。
  • 堵塞调用线程,直至另一线程就条件变量 cond 发出信号。
  • 重新锁定 mutex。

设计 pthread_cond_wait()执行上述步骤,是因为通常情况下代码会以如下方式访问共享变量
在这里插入图片描述

[Butenhof, 1996]指出,在某些实现中,先解锁互斥量再通知条件变量可能比反序执行效率要高.。如果仅在发出条件变量信号后才解锁互斥量,执行pthread_cond_wait()调用的线程可能会比在互斥量量仍处于加锁状态时就醒来,当其发现互斥量仍未解锁,会立即再次休眠。这会导致两个多余的上下文切换(context switch)。有些实现运用等待变形(wait morphing)技术解决了这一问题:将等待接收信号的线程从条件变量的等待队列转移至互斥量等待队列。这样,即便互斥量处于加锁状态,也无需切换上下文

在这里插入图片描述

测试条件变量的判断条件

每个条件变量都有与之相关的判断条件,涉及一个或多个共享变量。注意这里一般有一个通用的设计原则:必须由由一个 while循环,而不是 if 语句,来控制对 pthread_cond_wait()的调用。

这是因为,当代码从pthread_cond_wait()返回时,并不能确定判断条件的状态,所以应该立即重新检测判断条件,在条件不满足的情况下继续休眠等待。

从pthread_cond_wait()返回时,之所以不能对判断条件的状态做任何假设,理由如下:

  • 其他线程可能会先醒来。也行由多个线程在等待获取与条件变量相关的互斥量。即使就互斥量发出通知的线程将判断条件置为预期状态,其他线程依然有可能率先获取互斥量并改变相关共享变量的状态,进而改变判断条件的状
  • 可能会发生虚假唤醒的情况。在一些实现中,即使没有任何其他线程真地就条件变量发出信号,等待此条件变量的线程仍有可能醒来。在一些多处理器系统上,为确保高效实现而采用的技术会导致此类(不常见的)虚假唤醒。SUSv3 对此予以明确认可

C++11中的condition_variable

C++11中提供了condition_variable函数,实际上是对上面函数的封装。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥量结合在一起。

#include <condition_variable>

C++11中提供了两种封装:condition_variable和condition_variable_any

  • 相同点:两者都能与std::mutex一起使用。
  • 不同点:前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any的后缀。condition_variable_any会产生额外的开销。

一般只推荐使用condition_variable。除非对灵活性有硬性要求,才会考虑condition_variable_any。

条件变量的构造函数:


std::condition_variable::condition_variable
constructor:
    condition_variable();   //默认构造函数无参
    condition_variable(const condition_variable&) = delete;   //删除拷贝构造函数

条件变量的wait函数:


void wait( std::unique_lock<std::mutex>& lock );
//Predicate是lambda表达式。
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
//以上二者都被notify_one())或notify_broadcast()唤醒,但是
//第二种方式是唤醒后也要满足Predicate的条件。
//如果不满足条件,继续解锁互斥量,然后让线程处于阻塞或等待状态。
//第二种等价于
while (!pred())
{
    wait(lock);
}

例子

pthread_cond_wait的机制比较难里理解,是条件变量中重要的成分。条件变量用于线程间同步,那么pthread_cond_wait必须和互斥锁同时作用在一个线程里,它同时起到对资源的加锁和解锁,看下面的示例:

程序创建了2个新线程使他们同步运行,实现进程t_b打印9以内3的倍数,t_a打印其他的数,程序开始线程t_b不满足条件等待,线程t_a运行使a循环加1并打印。直到i为3的倍数时,线程t_a发送信号通知进程t_b,这时t_b满足条件,打印i值。

#include <iostream>
#include "hthread.h"
#include "hmutex.h"
#include "htime.h"

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;//init cond

int i = 1; //global
void *thread1(void *junk){

    for(;i < 9; i++){
        pthread_mutex_lock(&mutex);
        printf("call thread1, id = %lu \n",  pthread_self());
        if(i % 3 == 0){
            pthread_cond_signal(&cond);

        }else{
            printf("thread1: %d\n",i);
        }
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
// 0[2]
void *thread2(void*junk){
    for (; i < 9;){
        pthread_mutex_lock(&mutex);
        printf("call thread2 id = %lu \n",  pthread_self());
        if(i % 3 != 0){ // begin i = 1, 1 %2 != 0
            pthread_cond_wait(&cond, &mutex);
        }

        printf("thread2: %d\n",i);
        pthread_mutex_unlock(&mutex);

        sleep(1);
    }
}



int main(int argc, char* argv[]) {

    printf("call main id = %lu \n",  pthread_self());

    pthread_t t_a;
    pthread_t t_b;//two thread

    pthread_create(&t_a,NULL,thread2,(void*)NULL);  // when creat thread, thread2 run
    pthread_create(&t_b,NULL,thread1,(void*)NULL);//Create thread

    pthread_join(t_b,NULL);//wait a_b thread end
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}
call main id = 139821601752896 
call thread1, id = 139821571933952 
thread1: 1
call thread2 id = 139821580326656 
call thread1, id = 139821571933952 
thread1: 2
call thread1, id = 139821571933952 
thread2: 3
call thread1, id = 139821571933952 
thread1: 4
call thread2 id = 139821580326656 
call thread1, id = 139821571933952 
thread1: 5
call thread1, id = 139821571933952 
thread2: 6
call thread1, id = 139821571933952 
thread1: 7
call thread2 id = 139821580326656 
call thread1, id = 139821571933952 
thread1: 8

示例的解释:
call thread2:

  • 是线程2即t_b首先上锁,即 pthread_mutex_lock(&mutex);
  • 锁住了mutex使得此进程执行线程2中的临界区的代码,当执行到45行:if(i%3 != 0),此时i=1,满足此条件,则执行46行: pthread_cond_wait(&cond,&mutex);
  • 这句是关键,pthread_cond_wait(&cond,&mutex)操作有两步,是原子操作:
    • 第一 解锁,先解除之前的pthread_mutex_lock锁定的mutex;
    • 第二 挂起,阻塞并在等待对列里休眠,即线程2挂起,直到再次被唤醒,唤醒的条件是由pthread_cond_signal(&cond);发出的cond信号来唤醒。

call thread1:

  • 由于pthread_cond_wait已经对线程2解锁,此时另外的线程只有线程1,那么线程1对mutex上锁,
  • 若这时有多个线程,那么线程间上锁的顺序和操作系统有关。

thread1: 1:

  • 线程1上锁后执行临界区的代码,当执行到if(i%3 == 0)此时i=1,不满足条件,则pthread_cond_signal(&cond);
  • 不被执行,那么线程2仍处于挂起状态,输出thread1: 1后线程1由pthread_mutex_unlock(&mutex);解锁。

thread1: 2: 这时此进程中只有2个线程,线程2处于挂起状态,那么只有线程1,则线程1又对mutex上锁,此时同样执行临界区的代码,而且i=2,不满足条件,pthread_cond_signal(&cond);不被执行,那么线程2仍处于挂起状态,输出thread1: 1后线程1由pthread_mutex_unlock(&mutex);解锁。

call thread1:同样由线程1上锁,但此时i=3,满足条件pthread_cond_signal(&cond)被执行,那么pthread_cond_signal(&cond)会发出信号,来唤醒处于挂起的线程2。pthread_cond_signal同样由两个原子操作:1,解锁;2,发送信号;解锁即对线程1解锁,解除对mutex的上锁。发送信号,即给等待signal挂起的线程2发送信号,唤醒挂起的线程2。

thread2: 3:由于pthread_cond_signal唤醒了线程2,即i=3满足条件,pthread_cond_wait(&cond,&mutex);被执行,那么pthread_cond_wait(&cond,&mutex)此时也有一步操作:上锁;即对线程2上锁,此时的pthread_cond_wait(&cond,&mutex)的操作相当与pthread_mutex_lock(&mutex);那么线程2继续执行上锁后的临界区的代码,并由pthread_mutex_unlock(&mutex);对线程2进行解锁。

剩下的输出原理和上面解释的一样。

纵观pthread_cond_wait,它的理解不可之把它看作一个简单的wait函数,它里面应该是一族函数,不同的函数在不同的条件下执行,理解pthread_cond_wait的机制可以很好的学习条件变量。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
shell是一种完整的编程语言,易学易用;shell脚本可以使烦人的手工作业自动化,使你的工作变得简单、轻松!本书通过各种实用的系统管理小技巧和趣味脚本代码,详尽地介绍了shell编程各个不同的方面。学完本书后,你将成为一名shell编程高手。 内容简介 本书共分五部分 ,详细介绍了SHELL编程技巧,各种UNIX命令及语法,还涉及了UNIX的文字处理以及少量的系统管理问题。本书内容全面,文字简洁流畅,适合SHELL编程人员学习、参考。 目录 第一部分 SHELL 第一章 文件安全与权限 第二章 使用FIND和XARGS 第三章 后台执行命令 第四章 文件名置换 第五章 SHELL输入输出 第六章 命令执行顺序 第二部分 文本过滤 第七章 正则表达式介绍 第八章 GERP家族 第九章 AWK介绍 第十章 SED用法介绍 第十一章 合并与分割 第十二章 TR用法 第三部分 登录环境 第十三章 登录环境 第十四章 环境和SHELL变量 第十五章 小结 第四部分 基础SHELL编程 第十六章 SHELL脚本介绍 第十七章 条件测试 第十八章 控制流结构 第十九章 SHELL函数 第二十章 向脚本传递参数 第二一章 创建屏幕输出 第二二章 创建屏幕输入 第二三章 调试脚本 第二四章 SHELL嵌入命令 第五部分 高级SHELL编程技巧 第二五章 深入讨论 << 第二六章 SHELL工具 第二七章 几个脚本例子 第二八章 运行级别脚本 第二九章 CGI脚本

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值