条件变量变量也是出自POSIX线程标准,另一种线程同步机制。主要用来等待某个条件的发生。可以用来同步同一进程中的各个线程。当然如果一个条件变量存放在多个进程共享的某个内存区中,那么还可以通过条件变量来进行进程间的同步。

每个条件变量总是和一个互斥量相关联,条件本身是由互斥量保护的,线程在改变条件状态之间必须要锁住互斥量。条件变量相对于互斥量最大的优点在于允许线程以无竞争的方式等待条件的发生。当一个线程获得互斥锁后,发现自己需要等待某个条件变为真,如果是这样,该线程就可以等待在某个条件上,这样就不需要通过轮询的方式来判断添加,大大节省了CPU时间。

在互斥量一文中说过:互斥量是用于上锁,而不是用于等待;现在这句话可以加强为:互斥量是用于上锁,条件变量用于等待

条件变量声明为pthread_cond_t数据类型,在<bits/pthreadtypes.h>中有具体的定义。

1条件变量初始化和销毁

#include <pthread.h>
int pthread_cond_init (pthread_cond_t *cond,const pthread_condattr_t *cond_attr) ;
int pthread_cond_destroy (pthread_cond_t *cond);
两者的返回值都是:若成功则返回0,否则返回错误号

上面两个函数分别由于条件变量的初始化和销毁。

和互斥量的初始化一样,如果条件变量是静态分配的,可以通过常量进行初始化,如下:

pthread_cond_t mlock = PTHREAD_COND_INITIALIZER;

也可以通过pthread_cond_init()进行初始化,对于动态分配的条件变量由于不能直接赋值进行初始化,就只能采用这种方式进行初始化。那么当不在需要使用条件变量,释放底层空间之前,需要调用pthread_cond_destroy()销毁该条件所占用的资源。

2条件变量的使用

/*  等待条件变为真 */
int pthread_cond_wait (pthread_cond_t *cond,pthread_mutex_t *mutex); /* 限时等待条件为真 */
int pthread_cond_timedwait (pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime); /* 唤醒一个等待条件的线程. */
int pthread_cond_signal (pthread_cond_t *cond);  /* 唤醒等待该条件的所有线程 */
int pthread_cond_broadcast (pthread_cond_t *cond);

(1)pthread_cond_wait()函数用于等待条件被触发。该函数传入两个参数,一个条件变量一个互斥量,函数将条件变量和互斥量进行关联,互斥量对该条件进行保护,传入的互斥量必须是已经锁住的。调用pthread_cond_wait()函数后,会原子的执行以下两个动作:

  • 将调用线程放到等待条件的线程列表上,即进入睡眠;
  • 对互斥量进行解锁;

由于这两个操作时原子操作,这样就关闭了条件检查和线程进入睡眠等待条件改变这两个操作之间的时间通道,这样就不会错过任何条件的变化。

当pthread_cond_wait()返回后,互斥量会再次被锁住。

(2)pthread_cond_timedwait()函数和pthread_cond_wait()的工作方式相似,只是多了一个等待时间。等待时间的结构为struct timespec:

struct timespec{
time_t tv_sec //Seconds.
long tv_nsec //Nanoseconds.
};

函数要求传入的时间值是一个绝对值,不是相对值,例如,想要等待3分钟,必须先获得当前时间,然后加上3分钟。

要想获得当前系统时间的timespec值,没有直接可调用的函数,需要通过调用gettimeofday函数获取timeval结构,然后转换成timespec结构,转换公式就是:

timeSpec.tv_sec = timeVal.tv_sec;
timeSpec.tv_nsec = timeVal.tv_usec * 1000;

所以要等待3分钟,timespec时间结构的获得应该如下所示:

struct timeval now;
struct timespec until;
gettimeofday(&now);//获得系统当前时间 //把时间从timeval结构转换成timespec结构
until.tv_sec = now.tv_sec;
until.tv_nsec = now.tv_usec * 1000; //增加min
until.tv_sec += 3 * 60;

如果时间到后,条件还没有发生,那么会返回ETIMEDOUT错误。

     从pthread_cond_wait()和pthread_cond_timewait()成功返回时,线程需要重新计算条件,因为其他线程可能在运行过程中已经改变条件。

注意:pthread_cond_wait/pthread_cond_timewait函数的返回并不意味着条件的值一定发生了改变,必须重新检查条件的值。

pthread_cond_wait/pthread_cond_timewait函数返回时,相应的互斥量将被当前线程锁定。即使是函数出错返回。

一般一个条件表达式都是在一个互斥量的保护下被检查的。当条件表达式未被满足时,线程将仍然阻塞在这个条件变量上。当另一个线程改变了条件的值并向条件变量发出信号时,等待在这个条件变量上的一个线程或者所有线程被唤醒,接着都试图再次占有相应的互斥量。

阻塞在条件变量上的线程被唤醒以后,直到pthread_cond_wait函数返回之前,条件的值都有可能发生变化。所以函数返回以后,在锁定相应的互斥量之前,必须重新测试条件变量。最后的测试方式是循环调用pthread_cond_wait函数,并把满足条件的表达式置为循环的终止条件。例如:

pthread_mutex_lock();

while (condition_is_false)

 pthread_cond_wait();

pthread_mutex_unlock();

注意:pthread_cond_wait函数是退出点,如果在调用这个函数时,已有一个挂起的退出请求,且线程允许退出,这个线程将被终止并开始执行善后处理函数,而这时和条件变量相关的互斥量仍将处在锁定状态。(即pthread_cond_wait失败返回,但仍然获得互斥量,在清理函数注意解开该互斥量

(3)pthread_cond_signal() & pthread_cond_broadcast()

这两个函数都是用于向等待条件的线程发送唤醒信号,pthread_cond_signal()函数只会唤醒等待该条件的某个线程,pthread_cond_broadcast()会广播条件状态的改变,以唤醒等待该条件的所有线程。例如多个线程只读共享资源,这是可以将它们都唤醒。

这里要注意的是:一定要在改变条件状态后,再给线程发送信号。

考虑条件变量信号单播发送和广播发送的一种候选方式是坚持使用广播发送。只有在等待者代码编写确切,只有一个等待者需要唤醒,且唤醒哪个线程无所谓,那么此时为这种情况使用单播,所以其他情况下都必须使用广播发送。

唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定,如果线程的调度策略是SCHED_OTHER类型的,系统将根据线程的优先级唤醒线程。

3唤醒丢失问题

    
     在线程未获得相应的互斥量时调用pthread_cond_signal或pthread_cond_broadcast函数可能会引起唤醒丢失问题。
     唤醒丢失往往会在下面的情况下发生:
  • 一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;
  • 另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;
  • 没有线程正在处在阻塞等待的状态下。

4条件变量的属性设置

/* 初始化条件变量属性对象  */
int pthread_condattr_init (pthread_condattr_t *attr); /* 销毁条件变量属性对象 */
int pthread_condattr_destroy (pthread_condattr_t *attr); /* 获取条件变量属性对象在进程间共享与否的标识 */
int pthread_condattr_getpshared (const pthread_condattr_t *attr,int *pshared); /* 设置条件变量属性对象,标识在进程间共享与否 */
int pthread_condattr_setpshared (pthread_condattr_t *attr, int pshared) ;
示例代码:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count = 0; void *decrement_count(void *arg)
{
pthread_mutex_lock(&count_lock);
printf("decrement_count get count_lock\n");
while(count == 0)
{
printf("decrement_count count == 0 \n");
printf("decrement_count before cond_wait \n");
pthread_cond_wait(&count_nonzero, &count_lock);
printf("decrement_count after cond_wait \n");
printf("decrement_count count = %d \n",count);
} count = count + 1;
pthread_mutex_unlock(&count_lock);
} void *increment_count(void *arg)
{
pthread_mutex_lock(&count_lock);
printf("increment_count get count_lock \n");
if(count == 0)
{
printf("increment_count before cond_signal \n");
pthread_cond_signal(&count_nonzero);
printf("increment_count after cond_signal \n");
} count = count + 1;
printf("huangcheng \n");
printf("increment_count count = %d \n",count);
pthread_mutex_unlock(&count_lock);
} int main(void)
{
pthread_t tid1, tid2; pthread_mutex_init(&count_lock, NULL);
pthread_cond_init(&count_nonzero, NULL); pthread_create(&tid1, NULL, decrement_count, NULL);
sleep(2);
pthread_create(&tid2, NULL, increment_count, NULL); sleep(10);
pthread_exit(0); return 0;
}

运行结果:

huangcheng@ubuntu:~$ ./a.out
decrement_count get count_lock
decrement_count count == 0
decrement_count before cond_wait
increment_count get count_lock
increment_count before cond_signal
increment_count after cond_signal
huangcheng
increment_count count = 1
decrement_count after cond_wait
decrement_count count = 1
huangcheng@ubuntu:~$

说明:
等待线程
1.使用pthread_cond_wait前要先加锁
2.pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活
3.pthread_cond_wait被激活后会再自动加锁

激活线程:
1.加锁(和等待线程用同一个锁)
2.pthread_cond_signal发送信号
3.解锁
激活线程的上面三个操作在运行时间上都在等待线程的pthread_cond_wait函数内部。

即执行顺序:
等待线程->使用pthread_cond_wait前要先加锁
等待线程->pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活
激活线程->加锁(和等待线程用同一个锁)
激活线程->pthread_cond_signal发送信号
激活线程->解锁
等待线程->pthread_cond_wait被激活后会再自动加锁

UNIX环境高级编程——线程同步之条件变量以及属性的更多相关文章

  1. UNIX环境高级编程——线程同步之读写锁以及属性

    读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程.当然如果一个读写锁存放在多个进程共享的某个内存区中,那么还可以用来进行进程间的同步, 互 ...

  2. UNIX环境高级编程——线程同步之互斥锁、读写锁和条件变量(小结)

    一.使用互斥锁 1.初始化互斥量 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量 int pthread_mutex_init( ...

  3. UNIX环境高级编程——线程同步之互斥量

    互斥量(也称为互斥锁)出自POSIX线程标准,可以用来同步同一进程中的各个线程.当然如果一个互斥量存放在多个进程共享的某个内存区中,那么还可以通过互斥量来进行进程间的同步. 互斥量,从字面上就可以知道 ...

  4. UNIX环境高级编程——线程属性

    pthread_attr_t 的缺省属性值 属性 值 结果 scope PTHREAD_SCOPE_PROCESS 新线程与进程中的其他线程发生竞争. detachstate PTHREAD_CREA ...

  5. Unix 环境高级编程---线程创建、同步、

    一下代码主要实现了linux下线程创建的基本方法,这些都是使用默认属性的.以后有机会再探讨自定义属性的情况.主要是为了练习三种基本的线程同步方法:互斥.读写锁以及条件变量. #include < ...

  6. UNIX环境高级编程——线程和fork

    当线程调用fork时,就为子进程创建了整个进程地址空间的副本.子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量.读写锁和条件变量的状态.如果父进程包含多个线程,子进程在fork返回以后 ...

  7. UNIX环境高级编程——线程

    线程包含了表示进程内执行环境必需的信息,其中包括进程中标示线程的线程ID.一组寄存器值.栈.调度优先级和策略.信号屏蔽字.errno变量以及线程私有数据. 进程的所有信息对该进程的所有线程都是共享的, ...

  8. UNIX环境高级编程——线程和信号

    每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的.这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变.这样如果一 ...

  9. UNIX环境高级编程——线程私有数据

    线程私有数据(Thread-specific data,TSD):存储和查询与某个线程相关数据的一种机制. 在进程内的所有线程都共享相同的地址空间,即意味着任何声明为静态或外部变量,或在进程堆声明的变 ...

随机推荐

  1. NModBus的使用

    前言:最近在做一个项目,需要使用ModBus RTU与PLC进行通讯,现在将使用过程记录,以便备查. 一.什么是ModBus通讯协议 Modbus协议是应用于电子控制器上的一种通用语言,此协议支持传统 ...

  2. expect IDENTIFIER, actual IDENTIFIER 处理

    涉及到注入数据库的报错,这是很常见的了. 但是期望IDENTIFIER,实际IDENTIFIER 的报错,你们知道是什么意思吗? 我已开始看到的时候,是mybatis报错发神经了,报错了报错.再跑一次 ...

  3. 基本数据类型 异常 数组排序 JVM区域划分

               Day01 1.基本数据类型各占几个字节 Byte 1 short2 int4 long8 float4 double6 char2 boolean1 Byte b1=3,b2= ...

  4. C/C++知识大纲(只有作者看得懂的笔记)

    c标准库部分冷门函数. stdio.h char buf[256]; sscanf(buf,"%s",&buf); sprintf(buf,"Name: %s&q ...

  5. 原生Js写轮播图代码

    html css js 在知道jQuery如何实现轮播效果的基础上,用js写代码 如图:标记这里的地方 理解一下 用到的知识: 1.HTML DOM 的appendChild() 和 removeCh ...

  6. Mac下配置远程Linux 服务器SSH密钥认证自动登录

    1. 在本地机器创建公钥 打开万能的终端,执行如下命令,无视一切输出,一路欢快地回车即可. ssh-keygen -t rsa -C 'your email@domain.com' -t 指定密钥类型 ...

  7. Node.js Buffer(缓冲区)

    JavaScript 语言自身只有字符串数据类型,没有二进制数据类型. 但在处理像TCP流或文件流时,必须使用到二进制数据.因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门 ...

  8. MySQL LIKE 子句

    MySQL LIKE 子句 我们知道在MySQL中使用 SQL SELECT 命令来读取数据, 同时我们可以在 SELECT 语句中使用 WHERE 子句来获取指定的记录. WHERE 子句中可以使用 ...

  9. Docker 容器格式

    最初,Docker 采用了 LXC 中的容器格式.自 1.20 版本开始,Docker 也开始支持新的 libcontainer 格式,并作为默认选项. 对更多容器格式的支持,还在进一步的发展中.

  10. Python3 OS 文件/目录方法

    os 模块提供了非常丰富的方法用来处理文件和目录.常用的方法如下表所示: 序号 方法及描述 1 os.access(path, mode) 检验权限模式 2 os.chdir(path) 改变当前工作 ...