线程互斥与同步(part2)—互斥锁(Mutex)的“cp”:条件变量(Condition Variable)


前言


在博主的上篇博文中介绍了linux线程互斥的原因,并引入了互斥锁(Mutex)来解决问题,点击这里听互斥锁讲述它的的故事:linux线程互斥与同步(part1)—互斥锁(mutex)的原理及其实现机制

那么本篇博客就继续介绍关于linux线程互斥的另一个重要概念:条件变量(Condition Variable)


条件变量(Condition Variable)和互斥锁(Mutex)的“cp”关系


有句话叫“既生瑜,何生亮”,虽说互斥锁和条件变量都是为了维持线程间的互斥与同步。但他们可不是’瑜’和’亮’的关系。确切的说,他们俩是一对‘cp’

首先,我们想象这样一个场景,有两个人,一个往盘子里放苹果;另一个从盘子中取苹果。

如果我们将盘子看作临界资源,把这两个人当作两个线程。加入互斥锁后,就形成了对盘子的互斥访问。
如果一个人拿到锁进入临界区放苹果,此时另一个人也来申请锁想拿苹果。那么这个人代表的线程就会被阻塞。在第一个人拿锁和解锁之前的整个过程中,第二个人只能一直申请访问互斥锁,直到第一个人解开锁。

1

线程进入临界区之前要先访问互斥锁,像上述例子中由于两个人的优先级不同,优先级高的线程不断重复“拿锁-进入临界区-放锁”的过程,且不做实质性的工作(占着茅坑不拉屎)。导致优先级低的线程得不到时间片来访问互斥锁。这样就会形成线程的“饥饿”问题。
为了解决这种问题,我们要保证对互斥锁的访问按某种顺序进行;使线程之间协同合作,这就是线程同步

条件变量就是保证线程同步的一剂良药。

它提供了一种通知机制:在优先级高的线程放锁后立即通知别的线程取锁,若优先级高的线程想再次申请锁,只能在条件变量上挂起等待,这样就杜绝了优先级高的线程长时间霸占锁资源,实现线程间同步。
其实质是用变量的形式来表示当前条件是否成熟,标志资源状态。从而方便线程之间协作运行。

有了条件变量,上边的例子就会变成这样:
2

总结一下:单纯的互斥锁用于短期锁定,主要是用来保证线程对临界区的互斥进入。而条件变量则用于线程的长期等待,直至所等待的资源成为可用的资源。

所以,一个Condition Variable总是和一个Mutex搭配使用(地表最强cp)。


Code(代码举例)


知道了条件变量的概念,下面我们编写代码深入了解其作用。

首先了解一下条件变量主要的接口函数

1

2

一个线程可以调用 pthread_cond_wait函数在一个Condition Variable上阻塞等待,这个函数做以下三步操作:
1. 释放Mutex
2. 阻塞等待
3. 当被唤醒时,重新获得Mutex并返回

3

具体唤醒多少线程与问题规模有关。一个线程可以调用 pthread_cond_signal唤醒在某个Condition Variable上等待的另一个线程,也可以调用 pthread_cond_broadcast唤醒在这个Condition Variable上等待的所有线程。

4

代码目的:实现基于单链表模式的生产者-消费者模型
利用生产者-消费者模型和链表结构,生产者生产一个结点串在链表的表头上,消费者从表头取走结点。


科普时间:生产者与消费者模型?

5


知道了这些基本概念,现在我们来编写代码:

step1: 构建交易场所(临界资源):链表

6
在编写链表基本操作Init,PushHead,PopHead,Destory函数后,运行程序:

2

step2:创建生产消费者:2个线程

7

step3:保证互斥与同步:加入互斥锁保证线程之间的互斥访问

8

9

10

11

运行程序:

10

12
可以看到,虽然实现了互斥,但生产与消费并非间接进行,所以现象是长时间一直在生产,或一直在消费。这与实际情况不符。

step4:加入条件变量实现线程同步

我们对代码进行改造,如果生产者生产慢(sleep(1)),让消费者一直消费,且是无效消费(-1),
14
就会出现下边的情况:
13
这是因为虽然消费者是无效消费,但它一直占用锁。导致生产者没有时间生产。这是由于对交易场所的状态一无所知导致的

所以由开始对条件变量作用的分析,结合刚才的结果,可以知道消费者也可能像优先级高的线程那样,有“拿锁-进入临界区(取数据)-放锁”的不断重复过程,但消费者将链表数据取完后就不应该再取了,所以要用条件变量来表示交易场所的状态(链表满或不满)。

所以在程序中加入条件变量,在生产函数里加入唤醒函数,消费函数里加入等待函数:
14
程序运行效果如下:
16

可以看到,尽管消费函数先运行只,但它在判断链表为空后只能在条件变量上挂起等待。只有在生产者生产后消费者才能消费,且生产一条,消费一条。

注:这里的pthread_cond_wait函数参数中的锁并非表示线程抱着锁挂起,而是释放锁之后再挂起

step4: 另外,如果等待函数调用失败,还是会非法消费,所以加入检测:把 if -> while
18

至此,条件变量结束。


The End


其实仔细想想,上边的程序实际上是实现了一个栈,它符合后进先出的原则。

而且上述程序是生产1条,消费1条。且是在生产里边唤醒消费。其实我们还可以实现生产多条,通知1次的机制,只需要在生产函数里加入计数器即可。

另外,还可以实现生产者与消费者的互相通知唤醒机制,只需在程序中再加入一个条件变量,然后在消费函数里加入它的唤醒函数。

虽然 ‘cp’搭配,干活不累,但有时候单身狗的力量更强大(诶呀,好像剧透了>~<),好吧。本系列下一篇要介绍的就是“单身狗”—信号量的故事。

23

点击这里:linux线程互斥与同步(part3)—解决线程同步的”扛把子“:单身狗-信号量( Semaphore)一起见证信号量的心路历程。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值