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

简介: 如同互斥量和读写锁一样,条件变量也需要初始化和回收#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月前
多线程学习之解决线程同步的实现方法
多线程学习之解决线程同步的实现方法
21 0
|
1月前
|
安全 C++ 开发者
【C++多线程同步】C++多线程同步和互斥的关键:std::mutex和相关类的全面使用教程与深度解析
【C++多线程同步】C++多线程同步和互斥的关键:std::mutex和相关类的全面使用教程与深度解析
18 0
|
2月前
|
Java 调度
详解线程同步和线程互斥,Java如何实现线程同步和互斥
详解线程同步和线程互斥,Java如何实现线程同步和互斥
24 0
|
3月前
|
Java
Java多线程同步锁、Lock锁和等待唤醒机制及代码演示
Java多线程同步锁、Lock锁和等待唤醒机制及代码演示
|
4月前
|
Linux C语言
一个简单案例理解为什么在多线程的应用中要使用锁
一个简单案例理解为什么在多线程的应用中要使用锁
17 0
|
6月前
|
监控 安全 Java
自旋锁的伪代码实现,CAS的ABA问题,JUC常见类:Callable,ReentrantLock,线程创建方法的总结,信号量,原子类的应用场景,特定场所的组件CountDomLatch,针对集合类的
自旋锁的伪代码实现,CAS的ABA问题,JUC常见类:Callable,ReentrantLock,线程创建方法的总结,信号量,原子类的应用场景,特定场所的组件CountDomLatch,针对集合类的
|
安全 算法 Linux
Linux多线程:线程安全、线程互斥、死锁、线程同步
Linux多线程:线程安全、线程互斥、死锁、线程同步
121 0
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(下)
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(下)
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(下)
|
安全
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(上)
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(上)
多线程编程核心技术-对象及变量的并发访问-synchronize同步方法(2)(上)
|
设计模式 安全 算法
Java多线程(二)、线程的生命周期、线程的同步、Synchronized的使用方法、同步代码块、同步方法、同步机制中的锁、同步的范围、Lock(锁、不会释放锁的操作、单例设计模式之懒汉式(线程安全)
Java多线程(二)、线程的生命周期、线程的同步、Synchronized的使用方法、同步代码块、同步方法、同步机制中的锁、同步的范围、Lock(锁、不会释放锁的操作、单例设计模式之懒汉式(线程安全)
Java多线程(二)、线程的生命周期、线程的同步、Synchronized的使用方法、同步代码块、同步方法、同步机制中的锁、同步的范围、Lock(锁、不会释放锁的操作、单例设计模式之懒汉式(线程安全)