条件变量学习笔记

202 阅读5分钟

最近又开始复习操作系统了。条件变量是很多异步编程要用到的。之前学的没有总结总是忘记,趁这个机会记录一下。

弄清条件变量我们首先要理解条件变量试图解决的问题是什么。条件变量首先也是解决多线程访问同一个变量带来的竞态条件的一种手段。与自旋锁不同的是条件变量在获得不到锁的时候并不会抢占cpu进行自旋,而是将获得不到锁的线程置于休眠状态,当获得锁的线程释放锁的时候再将试图获得锁的线程唤醒。试图获得锁的调用wait()函数,如果可以获得则可以继续进行反之休眠,获得锁的线程使用结束后调用signal()唤醒被休眠的线程。wait()函数的作用就是原子的释放锁(假定在调用wait之前已经得到了锁)然后将该线程休眠。

知道了条件变量的目的之后为了实现这个目的解决不同条件下的竞态条件我们需要添加额外的数据和操作来实现目的。

首先我们先看一个条件变量使用的demo级别的代码。代码来自github.com/remzi-arpac…


1. 使用flag字段

这个字段主要解决的是试图获得锁的线程没有获得锁要休眠的程序还没有进入休眠的时候发生线程切换,释放锁的线程调用singal() 但是试图获得锁的线程接受不到的问题。这个问题发生的具体流程如下。

A B 两个线程。A创建了B线程

1. A创建B线程,试图在B线程结束的时候唤醒再进行下一步操作

2. B被创建后立刻执行然后调用singal试图唤醒wait线程(但是此时此刻并没有,因为A线程还没有运行到wait的代码段)

3. 切换回A 进入休眠,但是唤醒的signal已经发出过。A 不会被唤醒。

可以看出问题存在于A进行休眠前并不知道锁的状态已经发生了变化。而解决的方法就是加入可以判断状态的字段。加入了flag字段后就可以在第四步将要进入休眠前检验flag字段是否已经被修改,如果已经被修改则不用进行休眠可以直接向下进行各种操作。具体流程如下。

更改后
A B 两个线程。A创建了B线程

1. A创建B线程,试图在B线程结束的时候唤醒再进行下一步操作

2. B被创建后立刻执行然后调用singal试图唤醒wait线程(但是此时此刻并没有,因为A线程还没有运行到wait的代码段),并修改flag字段

3. 切换回A,A检测flag字段已经被修改,A不进行休眠,继续向下执行代码。

2. flag字段搭配mutex使用

在所有的条件变量的使用中都需要搭配一个互斥量使用。使用mutex的原因是因为解决某些条件的race condition。如果不使用mutex在下列情况下就回出现问题。

A B 两个线程。A创建了B线程

1. A创建B线程,试图在B线程结束的时候唤醒再进行下一步操作

2. A运行到条件判断处,检查flag不满足条件时将要进入wait状态的时候发生context switch,切换到线程B。

3. 线程B完成工作更改flag值,并发出singal(此时A并没有进入睡眠)试图唤醒睡眠线程。

4.切换回A,由于A已经进行了判断会进入wait状态等待唤醒信号(但是已经发过了)。A不会被唤醒。

在加入了mutex之后在发生context switch的时候不会出现flag值变化的情况,问题就解决了。

3. 使用while语句代替if语句

这个操作主要是为了解决虚假唤醒的问题。首先解释一下什么是虚假唤醒,虚假唤醒顾名思义就是在你唤醒的时候你被唤醒的条件已经不被满足了。举一个非常典型的例子就是生产消费者模型。生产者在消费buffer空的时候被唤醒生产物品,buffer满了以后进入休眠并唤醒消费者。消费者消费buffer中的物品,并在buffer为空的时候唤醒生产者,自己进入休眠状态。这个逻辑在只有一个生产者和消费者的时候是没有问题的,问题出现在多个消费者的时候。可能出现在你被唤醒的时候情况已经发生了变化。比如说某个线程收到唤醒signal后加入就绪队列后,等到这个线程应该执行的时候某个也被唤醒的线程抢先改变了状态(在这里就是将物品消费了)。等到这个线程执行的时候条件已经不满足了。所以需要用while循环来解决这个问题。具体流程如下。

A B C三个线程。A是生产者,BC是消费者

1. 一开始A工作,BC睡眠。A开始向buffer放入数据,并唤醒BC。

2. BC进入就绪队列,A继续生产。A生产结束后进入睡眠。

3. B抢先进入运行状态并将buffer中的数据消费,然后发信号,进入睡眠,

4. C从就绪队列转为运行态,但是这个时候buffer已经没有数据了。

再改用while后C运行的时候就会发现条件已经发生了变化,不应该再往下执行。继续切换为睡眠状态。