条件变量与互斥锁

为什么要引入条件变量

        我们假设一个场景,现在线程A需要“IO变量”变为W的时候,才能工作。而我们的线程B就是专门来将“IO变量”变为W的。在没有引入条件变量的时候,我们可能就会有这种写法:

IO=R;
//pthread A
while (IO != W)
	sleep(1);
pthread_mutex_lock(&mutex);		//加锁
// IO操作...
IO = R;
pthread_mutex_unlock(&mutex);	//解锁



//pthread B
while(IO!=R)
	sleep(1);
pthread_mutex_lock(&mutex);		//加锁
// IO操作...
IO = W;
pthread_mutex_unlock(&mutex);	//解锁

        但是,这个有一个弊端就是:每一个线程都会抢占到CPU的执行权之后,做了大量的无用操作。例如pthread A在获得了CPU的执行权之后,可能就在判断IO!=W。但是这个在pthread B没有执行的时候,此时IO永远都不可能为W。所以我们才说pthread A的判断是无效的。此时pthread A占用了CPU的资源就白白浪费了。(并且这个其实是可以不去sleep的,但是这样就更加浪费CPU的资源了。)

        为了减少上面的线程做的一些的无效的操作,我们可以想到的一个简单方式就是:增加每个线程睡眠时间。例如我们设置为10s,这样子我们可以减少线程占用CPU之后做无效操作。但是这个增加睡眠时间,其实也有弊端。因为我们无法控制这个睡眠时间设计多少才合适。可能会出现这样情况,我们设置了10s之后,pthread A开始进入睡眠状态,此时CPU的执行权到pthread B之后,只过了5s就把IO变量置为W了,并把CPU执行权交给了pthread A。但是由于此时pthread A还处于睡眠中,就白白浪费了这个时间。

        所以,循环检测条件有下面两个弊端:

1.检测条件间隔时间难以把握
2.设置过短浪费系统开销,过长则无法即时响应条件

所以我们引入了条件变量的这个概念。我们就可以利用系统提供给我们的条件变量来实现上面的IO这个变量的功能。所以对于上面的例子来说,我们就可以写出下面的代码:

IO=R;
//pthread A
pthread_mutex_lock(&mutex ); // 加锁
while (IO != W )
{
	//用条件等待取代 sleep
	pthread_cond_wait(&cond , &mutex );
}
// IO操作...
IO=R
pthread_mutex_unlock(&mutex ); // 解锁


//pthread B
pthread_mutex_lock(&mutex);
while(IO!=R)
{
	pthread_cond_wait(&cond,&mutex);
}
IO=W;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);

下面是我看到的一个说:pthread_cond_wait()被设计出来的初衷:

线程如果需要等待某个条件发生,它该作何处理呢?它可以重复对互斥对象锁定和解锁,每次都会检查共享元素,以查找某个值。这样会比较浪费cpu的时钟周期。而且效率比较低,何不借鉴一下dma的处理办法,等待某个条件发生了通知一下,这样cpu就可以处理自己的事了,我想pthread_cond_wait的初衷就是这样的,也是这样被设计出来的。如果你有不同解释可以通知我,我们可以共同讨论。

条件变量为什么要和互斥锁一起用

        看到上面的段代码,可能就会想:为什么使用pthread_cond_wait()的时候,需要传入一个已经加锁的mutex变量。
答:

因为条件变量也是临界变量,两个线程都是会去操作的。为了保证线程安全,我们对于临界变量的访问的时候,就是需要先加锁的。(但是这个其实并不能使大家信服,所以在大家看完pthread_cond_wait()的错误写法之后,我在后面会重新给出一个解释。)

因为我一开始学习的时候,就觉得条件变量和互斥锁是一个性质的,所以我就觉得不需要加锁。所以我们 假设pthread_cond_wait()是不需要传入这个锁变量的,所以pthtread_cond_wait()函数内部也没有什么解锁等操作,仅仅只是将该线程放入到等待队列中 所以我们可以写出下面的代码:(下面的代码不是采用上面的例子的)

ready=false;
//pthread A
while (ready==false) 
{     
	pthread_cond_wait(&cond); 
}
pthread_mutex_lock(&mutex);
//操作临界变量
pthread_mutex_unlock(&mutex);

//pthread B
ready=true;
pthread_cond_signal(&cond);

此时我们想像一下这样子的一个场景,在pthread A获得CPU的执行权之后,判断ready==false之后,进入while循环,还没有来的及执行pthread_cond_wait(&cond); CPU的执行权到pthread B这里,然后执行完了pthread_cond_signal(&cond);但是问题在于此时由于pthread A还没有执行pthread_cond_wait(&cond);所以,此时的唤醒没有将phthread A唤醒。所以就出现了 唤醒丢失 问题。

        当然我说上面的例子仅仅只是说明我们需要互斥量来操作条件变量。至于为什么在使用条件变量的时候,我们需要加锁,就是因为条件变量是一个临界变量,对于临界变量的操作在多线程中是需要加锁的。

pthread_cond_wait()的错误写法

1.错误写法

        这段内容是复制的这个文章的内容。https://zhuanlan.zhihu.com/p/55123862

//初始变量:
bool ready = false;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

//Thread A
pthread_mutex_lock(&mutex);
while (false == ready) 
{
 	pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

//Thread B
ready = true;
pthread_cond_signal(&cond);

2.怎么错了

        证明一个东西是错的,简单,给出一种错误的 case 就好,如下的执行序列 Thread A 就会丢失 Thread B 的条件变量的唤醒永久 wait。在 ready = false 的时候,Thread A 进入 while 循环,但是还没有执行 wait 的时候,thread B 执行了 ready = true 和 signal 唤醒,那么就出现条件变量唤醒 signal 先于 wait,那么相当于 Thread A 还没有被加入唤醒队列,这个时候,你已经 signal 唤醒了,那么这次唤醒自然就丢失了,执行序列的第 5 行,也就是 Thread 的第 3 行pthread_cond_wait(&cond, &mutex) 就会一直等在那里了。上述执行过程就是下面的这个表格的样子:
在这里插入图片描述

3.错在哪里

        核心出错的原因就是一个点,当判断 ready 为 false 进入 while 循环(Thread A 第 2 行),调用 pthread_cond_wait (Thread A 第 3 行)之前,Thread B 修改了 ready 为 true,并且 signal 了条件变量,导致 signal 先于 wait,出现丢失。 这里根本的原因就是没有保证 ready == false 判断成立和 Thread A 调用 wait 进入唤醒队列的原子性,也就是我一旦判定 ready 为 false,那么必须进入等待队列,且在这期间不允许有人修改 ready 为 true,并执行 signal 操作 。所以正确使用条件变量有两个约束:

(1) 保证 ready == false 判断成立和 Thread A 调用 wait 进入唤醒队列的原子性
(2) 设置 ready = true 在 signal 唤醒之前,也就是 Thread B 的 1、2 两行的顺序绝对不能反过来

上面错误的写法仅仅满足了约束 (2),但是却确忽略了(1)。一般来说约束(2)大部分人都能够意识到,因为错误 case 比较容易想到。因为大家都知道需要将条件改变之后再去唤醒,也就是先执行ready = true;然后执行pthread_cond_signal(&cond);

        上面错误的写法虽然给 Thread A 加锁了,但是这个锁加得有点糊里糊涂,没弄明白为什么要给 pthread_cond_wait(&cond, &mutex) 传递一个互斥量,以为是 pthread_cond_t 内部存在竞态条件,其实并不是,pthread_cond_wait(&cond, &mutex)调用之前会加锁,然后在内部将 thread 加入唤醒队列,然后才释放锁,其实就是为了保证约束(1),显然仅仅 Thread A 加锁,而 Thread B 设置 ready = true 没有加锁,并不能保证约束(1)的原子性。所以为了满足约束(1),需要给 Thread B 设置 ready = true 也加锁,正确的写法如下:

//Thread A
pthread_mutex_lock(&mutex);
while (false == ready)
{
     pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

//Thread B
pthread_mutex_lock(&mutex);
ready = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);

        这样就保证了原子性约束(1),那么无论线程如何运行,都只会有只有两种情况,情况 1:如果 Thread A 先拿到 mutex,那么此时 ready 为 false,Thread A 调用 pthread_cond_wait 进入等待队列,接着释放 mutex,然后 Thread B 才能修改 ready,并 signal;情况 2:如果 Thread A 没有拿到 mutex,Thread B 拿到 mutex,然后修改 ready 为 true,然后释放锁,这样 Thread A 在拿到 mutex,就不会再进 while 循环调 wait 了。

        看完了上面那个例子,我现在重来来解释一下:为什么条件变量要和互斥锁一起使用。

a) 如果对一个共享变量需要多线程操作的时候,我们就需要用到锁。如果我们对这个共享变量不仅仅是需要简单的数值计算(例如加法和减法),而是需要这个共享变量达到某种状态之后我们进行才能进行别的操作。此时如果完成这个需求的话,我们就需要使用条件变量。当然,你使用锁也是可以完成的,但是效率不高。因为你要么不断的轮询来查看这个共享变量的状态是否达到要求,但这显然就白白耗费了CPU资源;要么采用先睡眠一段时间,然后查看该变量的状态。但是显然这个睡眠的时间是不好确定的。所以对于这种情况,我们需要的就是:进程直接阻塞,但是这个变量的状态出现我想要的之后,别人来将我唤醒。这样子就可以最大的节省CPU资源和进程效率的最大化。这点上面的例子可以说明这点了。

b) 因为条件变量所关心的状态必然也是一个共享变量所引起的。因为如果这个状态不是由共享变量引起的,那么说明这个变量只有你这个线程可以访问。那么我就不需要对他进行互斥访问了,那么我就不需要轮询了。因为这个变量只有我自己一个线程能够改动,那我肯定知道什么时候他达到要求了。

c) 正因为这个条件变量所关心的也是一个共享变量,所以在访问它之前肯定要加锁。而我们肯定是先while()访问,发现没有达到状态,所以要进入阻塞。而在while之前,我们就需要加锁。所以,也就是说,因为在采用pthread_cond_wait()之前,都是需要对一个状态量进行判断的,而这个状态量肯定又是一个共享变量。而对共享变量的访问是需要加锁的,所以形成了我们必须要在调用pthread_cond_wait()之前给互斥锁加锁。如果你还是有疑惑,你可以看看你自己之前写的需要使用条件变量的代码上,是不是调用pthread_cond_wait()之前,都需要判断一个状态量,而这个状态量又肯定是一个共享变量。通过你自己写的代码来验证自己的猜想,肯定是具有说服力的。

d)最后,我们借助一下pthread_cond_wait()内部的分解操作来更加深入的理解一下。

pthread_cond_wait()的分解

        pthread_cond_wait()函数内部做了下面三个事情:

1)pthread_cond_wait所做的第一件事就是同时对传入的互斥对象mymutex解锁(这样其它线程就可以修改共享对象了,操作之前不要忘记加锁哦)。
2)阻塞该线程
3)调用pthread_cond_wait的线程被叫醒之后,将重新给mymutex加锁之后才返回

之所以这样设计三个步骤,我觉得就是为了实现上面说的约束1,也就是: 保证 ready == false 判断成立和 Thread A 调用 wait 进入唤醒队列的原子性 。当然,我们这里实现约束1还需要:唤醒者在调用pthread_cond_broadcast或pthread_cond_signal唤醒等待者之前也必须对相同的mutex加锁。

判断的时候为什么采用while而不是采用if

        我们看到下面的这个代码:

//Thread A
pthread_mutex_lock(&mutex);
while (false == ready)
{
     pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

//Thread B
pthread_mutex_lock(&mutex);
ready = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);

我们可能会想,为什么这里一定要使用while来判断呢?为什么不采用if呢?这个是因为可能有多个Thread A这样的子的线程(例如Thread C),此时虽然pthread_cond_signal(&cond);只能唤醒一个线程,并且唤醒了C进程,然后消耗了资源。此时CPU执行权到Thread A这里之后,因为采用的是if来判断,我们就直接出走这个判断了,即使这里还不能出循环,因为刚刚的资源已经被Thread C用掉了。所以为了避免这种情况的出现,我们还是使用while()来判断,而不是if来判断。

我自己的猜想

        这段的内容可能不正确,都是我自己的想法。要是大家有想法的话,可以在下方留言。
        我就在想,在调用了pthread_cond_wait()之后,我们知道线程A是睡眠了,但是之后线程B调用了pthread_cond_signal()之后,就可以将这个线程唤醒。但是问题就是,线程A怎么就知道条件变量已经被改变了,它是不是也是内部在采用while循环在持续检查条件变量的值呢?如果真的就是这样子的话,那么这样子和我最开始提出的循环检测的方式又有什么不同呢?
        答:这里应该不是采用while循环来检查的。线程A调用pthread_cond_wait()之后,内核就将其挂在了一个睡眠的队列上面,并且这个队列上面的都是因为同一个条件变量而睡眠的。而线程B先改变条件变量的值,然后调用了pthread_cond_signal(),内核就将这个条件变量上的一个线程唤醒。那么此时我们就可以确保线程在被唤醒之后,可以得到被改变之后的条件变量的值。

参考文章

条件变量 之 稀里糊涂的锁
互斥锁和条件变量的区别与应用
pthread_cond_wait详解
pthread_cond_wait详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值