线程同步:条件变量的使用细节分析

简介: 如同互斥量和读写锁一样,条件变量也需要初始化和回收#includeint pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);互斥量和读写锁解决了多线程访问共享变量产生的竞争问题,那么条件变量的作用何在呢。
如同互斥量和读写锁一样,条件变量也需要初始化和回收
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);


互斥量和读写锁解决了多线程访问共享变量产生的竞争问题,那么条件变量的作用何在呢。


条件变量的作用在于他给多个线程提供了一个汇合的场所。什么意思呢?
举个最简单的例子,比如运动会赛跑中,所有选手都会等到发令枪响后才会跑,吧选手比作
其他的子线程。发令员比作主线程。 那么就是说,所有的子线程现在都在等待主线程给予
一个可以运行的信号(发令枪响)。这就是这些子线程的汇合点。如果主线程没给信号,那么子线程就会阻塞下去。

大概明白了 条件变量的作用,现在我们来考虑 第一个使用细节上的问题

考虑一个情况:b c d 三个线程都期望在一个条件变量等待主线程发送信号,如果此时条件测试为假,那么三个线程下一步应该是阻塞休眠。
但是在判断条件不正确和休眠这之间有个时间窗口,假如在bcd三个线程检查条件为假后,cpu切换到另一个线程A,
在线程A中却使条件变为真了。那么当cpu切换回bcd线程中时线程还是会休眠。也就是说在线程检查条件变量和进入休眠等待
条件改变这两个操作之间存在一个时间窗口。这里存在着竞争。


我们知道互斥量是可以用来解决上面的竞争问题的,所以条件变量本身 是由互斥量来保护的
既然判断和睡眠是由互斥量来保护从而成为一个原子操作,那么其他改变条件的线程就应该以一致的方式修改条件
也就是说其他线程在改变条件状态前也必须首先锁住互斥量。(如果修改操作不是用互斥量来保护的话,那么判断和休眠使用互斥量来保护也就没有意义。因为 其他线程还是可以在两个操作的空隙中改变条件。但是如果修改操作也使用互斥量。因为判断和休眠之前先加锁了。那么修改操作就只能等到判断失败和休眠两个操作完成才能进行 而不会在这之间修改)

下面是提供的接口:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restricr mutex,
const struct timespec *restrict timeout);

使用pthread_cond_wait等待条件变为真,传递给pthread_cond_wait的互斥量对条件变量进行保护,调用者把锁住的互斥量传递给函数。

(互斥量在传递给函数之前已经调用pthread_mutex_lock锁住
函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁。这样使得判断和休眠成了原子操作。也就关闭了他们之间的
时间窗口。

当pthread_cond_wait返回时,会重新获取互斥量(互斥量再次被锁住)。
 
 pthread_cond_timedwait与pthread_cond_wait的区别在于它指定了休眠的时间,如果时间到了,但是条件还是没有出现,那么pthread_wait_timedwait也将
 重新获取互斥量。然后返回 错误ETIMEDOUT
 需要注意的一点是。pthread_cond_timedwait的参数timeout不是相对值,而是绝对值。比如你想最多休眠三分钟,那么timeout不是3分钟
 而是当前时间加上3分钟。
 
 有两个函数可以用来通知线程条件已满足。pthread_cond_signal函数将唤醒等待该条件的某个线程。
 pthread_cond_broadcast函数将唤醒等待该条件的所有线程。
 
 int pthread_cond_signal(pthread_cond_t *cond);
 int pthread_cond_broadcast(pthread_cond_t *cond);
 
 现在我们来看一个具体的例子。
 
 在下面这个程序中两个子线程在一个条件变量cond上等待条件 i 等于一亿成立。主线程中对 i 做自增操作,当i增加到一亿的时候。条件成立  那么主线程 向条件变量发送信号。那么两个子线程就会从休眠中醒来从而继续运行。
 
 pthread_mutex_t mutex;
pthread_cond_t  cond;
unsigned long i=0;


void *th1(void *arg){
        pthread_mutex_lock(&mutex);            //条件变量是由互斥量来保护的
     
        pthread_cond_wait(&cond,&mutex);
     
        pthread_mutex_unlock(&mutex);
        printf("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
        pthread_exit((void *)0);
}
void *th2(void *arg){
        pthread_mutex_lock(&mutex);
     
        pthread_cond_wait(&cond,&mutex);
     
        pthread_mutex_unlock(&mutex);
        printf("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n");
        pthread_exit((void *)0);
}
int main(void){
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);


        pthread_t t1,t2;
        pthread_create(&t1,NULL,th1,(void *)0);
        pthread_create(&t2,NULL,th2,(void *)0);


        while(1){       
                pthread_mutex_lock(&mutex);          // i为两个子线程等待的条件,就像上面说的修改它也应该先锁住互斥量
                i++;
                pthread_mutex_unlock(&mutex);
                if(i==100000000){
                        pthread_cond_broadcast(&cond);
                        break;
                }
        }
        pthread_join(t1,NULL);
        pthread_join(t2,NULL);


        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&mutex);
        exit(0);
}
 程序运行后停顿几秒输出:
 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
也就是主线程中不停的 对i 进行自增操作。当i等于一亿的时候,那么条件满足。主线程向条件变量发送信号。两个在条件变量杀上等待
的子线程收到信号后便开始运行。


但是 这个程序是存在问题的。这是我们要说的 第二个关于条件变量上的细节。

在两个子线程中 我们只是简单的使用了  pthread_cond_wait(&cond,&mutex); 在条件变量上休眠等待主线程发送信号过来。
但是在pthread_cond_wait调用等待的时候,线程是释放锁的。(当他返回时才会再次获得锁)。
那么就存在一个问题

假想一下。当主线程发送信号过来后。在子线程 在pthread_cond_wait上等待发现信号发过来了,那么子线程将醒来并运行(注意这个时候pthread_cond_wait 还未返回,那么锁是释放的,因为pthread_cond_wait在等待是会释放锁,返回时才会重新获得锁),那么如果这时候另一个线程改变了 i(对i进行了增减操作。)
那么此时i 不在是 一亿。但是切换到子线程时他并不知情,他会仍旧认为条件是满足的。也就是说 我们不应该仅仅依靠pthread_cond_wait的返回
就认为条件满足。
所以 上面的程序 中 子线程中的 pthread_cond_wait(&cond,&mutex) 应该改为:
while(i!=100000000){
pthread_cond_wait(&cond,&mutex);
}
这样即使 在子线程中 pthread_cond_wait返回前还未获得锁的这段空隙有其他线程改变了 i 使条件不在成立。那么当pthread_cond_wait返回时 他仍旧能发现 i 条件不成立。就会继续调用pthread_cond_wait再条件变量上等待。


最后再来看个上面的一个问题:


在给  在条件变量上等待的线程  发送信号的线程中有下面两个步骤;
a:
(1)对互斥量加锁(pthread_mutex_lock)
(2)改变互斥量保护的条件。(对应上面的例子就是在主线程中的 i++ 操作)
(3)向等待条件的线程发送信号(pthread_cond_broadcast)
(4)对互斥量解锁(pthread_mutex_unlock)
b:
(1)对互斥量加锁(pthread_mutex_lock)
(2)改变互斥量保护的条件。(对应上面的例子就是在主线程中的 i++ 操作)
(3)对互斥量解锁(pthread_mutex_unlock)
  (4)向等待条件的线程发送信号(pthread_cond_broadcast)
  
这两种步骤其实都是可以的 但是都存在一些不足。
在 a 步骤中。 也就是主线程在发送条件成立信号在解锁前。(上面给的例子是在解锁后,在b中会说明)
那么也就是主线程发送信号后还是持有锁的,当子线程收到信号后会结束休眠
但是前面说过pthread_cond_wait返回时会再次获得锁,但是主线程还并未释放
锁,所以会造成子线程收到信号开始运行并立即阻塞

在b步骤中。  主线程在释放锁后才发送信号。我们上面的例子就是这么做的。但是这也存在一个问题

但释放锁后,另一个线程很可能会在发送信号之前获得锁并修改 变量i 导致条件再次不成立
但是会到主线程中他却并不知情,导致仍会发送信号给子线程。子线程认为条件满足
从休眠中醒来开始运行,但此时条件是不满足的。
所以在上面的例子中我们将
pthread_cond_wait(&cond,&mutex) 改为:
while(i!=100000000){
pthread_cond_wait(&cond,&mutex);
}
让子线程醒来后再次判断条件是否成立。这样就可以避免了上面的问题。


总结一下: 条件变量的要点在于 他提供了一个让多个线程汇合的点。但是条件变量本身是需要
互斥量来进行保护的。
我们不能仅仅根据pthread_cond_wait返回就认为条件满足了。而需再次判断条件是否正确
相关文章
|
5月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
401 1
|
6月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
存储 NoSQL Redis
Redis 新版本引入多线程的利弊分析
【10月更文挑战第16天】Redis 新版本引入多线程是一个具有挑战性和机遇的改变。虽然多线程带来了一些潜在的问题和挑战,但也为 Redis 提供了进一步提升性能和扩展能力的可能性。在实际应用中,我们需要根据具体的需求和场景,综合评估多线程的利弊,谨慎地选择和使用 Redis 的新版本。同时,Redis 开发者也需要不断努力,优化和完善多线程机制,以提供更加稳定、高效和可靠的 Redis 服务。
352 1
线程CPU异常定位分析
【10月更文挑战第3天】 开发过程中会出现一些CPU异常升高的问题,想要定位到具体的位置就需要一系列的分析,记录一些分析手段。
356 0
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
1140 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
279 0
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
541 4
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
251 6
|
存储 Ubuntu Linux
C语言 多线程编程(1) 初识线程和条件变量
本文档详细介绍了多线程的概念、相关命令及线程的操作方法。首先解释了线程的定义及其与进程的关系,接着对比了线程与进程的区别。随后介绍了如何在 Linux 系统中使用 `pidstat`、`top` 和 `ps` 命令查看线程信息。文档还探讨了多进程和多线程模式各自的优缺点及适用场景,并详细讲解了如何使用 POSIX 线程库创建、退出、等待和取消线程。此外,还介绍了线程分离的概念和方法,并提供了多个示例代码帮助理解。最后,深入探讨了线程间的通讯机制、互斥锁和条件变量的使用,通过具体示例展示了如何实现生产者与消费者的同步模型。

热门文章

最新文章