OS: 同步原语
本文最后更新于17 天前,其中的信息可能已经过时,如有错误请发送邮件到ponsde333@gmail.com

线程之间的共享内存常常导致竞争,让我们探索一下应该如何防范竞争导致的问题:

我们看看哪些地方需要加锁来防范,其实也显而易见,就是会发生多方操作而导致互相竞争、出现覆盖的情况,此时就需要加锁来限制,我们称这个区域为临界区:

我们需要做的,就是在进入临界区和离开临界区的时候分别拿锁和释放锁。

一种方法是彼得森算法”,谦让别人先进入来避免竞争,但该算法在当前乱序执行的 CPU 下会出现失效。

turn、flag 是全局变量,两个线程共享。

算法的理念是,如果我想进,同时对方想进,那就让对方先进,flag 标记谁想进,而 turn, 就决定着让谁先进。

如果不谦让呢?让他们互相抢怎么样?假设线程0速度非常非常快,那就会让线程1一直卡在 while 循环,如刚判断完是否运行正准备进行下一次判断时,这短短的空隙被线程0钻了空子,导致线程1一直处于饥饿。

而谦让就不一样了,哪怕线程0再快,当发现线程1想走的时候就会让线程1走,这样能确保有限的等待,不会出现某个线程一直等等等,如果锁的实现没有公平性保证,那么速度快或调度更频繁的线程可能反复抢到锁,导致其他线程长期等待,形成饥饿。

除了皮尔森算法外,我们还有种操作叫做原子性操作。

在旧时候我们认为原子是最小的不可分的单位 (现在我们都知道还有夸克之类的),因此我们弄了个操作也称它为原子性,认为他是最小的不可分的操作。

比如喝水,我们的操作是拿起水杯,喝一口,放下水杯,分为了三步,而原子性操作的喝水时,要么我喝了水,要么我没喝水,没有中间拿起水杯、喝一口、放下水杯这种分布的动作。

当在真实的现代多核系统中,可靠、高效的原子操作通常需要硬件支持;纯软件算法在理想内存模型下可以成立,但在现代 CPU 上通常还需要内存屏障或原子语义配合。

接下来是锁,我们最常用的了。

最常用的锁是互斥锁,互斥,顾名思义只有一个能有另一个不能有。

当线程要进入临界区时,需要拿锁才能进去,出了临界区释放锁,如果有人拿着锁时,其他线程需要进入该临界区又需要锁,就需要等,直到拿到锁才会继续往后运行。

那么拿锁也就有讲究了,如果你是多个线程不断自旋着抢锁,那么就是自旋锁:

但问题也很明显,你一直抢万一某个线程运气不好,一直抢不到,不就要等到天荒地老,活生生饿死吗。

我们增加排号自旋锁,虽然还是不停的转着想要拿锁,但每个线程看着是自己的序号,如果到了自己这一号才能拿锁。

但我们也能发现,这种自旋式的等待是在浪费算力资源,等待的时间里只是在白白的空转:

那么我们何不将等待中的线程转为堵塞态,等到自己运行的时候再唤醒,避免占用资源呢?

互斥锁可以用于多个线程,只是同一时刻只允许一个线程进入临界区。如果希望同一时刻允许最多 N 个线程进入,则可以使用信号量:

P、V 信号量分别对应着减少信号量和增加信号量,这里是当 S > 0 的时候线程才能往后走。

这样我们就能做到让多个线程进入同一个临界区的情况,只需要让 S 的初始值 > 1 就可以。

在让多个线程进入同个临界区,我们探索下是否会发生冲突,如果每个线程都只是读操作,那再多的线程进入临界区都无妨,但只要是写,就会有竞争。

如果多个线程都在读,而一个线程想写,此时是什么呢?

思考偏向于读者的情况,一个线程想修改临界区里的数据,但有其他线程正在读,让写线程等读线程都走完了再去写,如果中途有其他读线程进来也让他进来读,但读线程源源不断地进来怎么办?写线程就得一直等。

这里我们能看到,当读者存在临界区时,写者的锁就被读者拿走了,只有等读者全走完时,写者的锁才会被释放,写者才能进来。

如果是偏向于写着呢?那就是写者来了,后续的读者不让进,等参与的读者离开后就进去写:

我们可以看到,当有写者想要进临界区时, has_writer 被标记为 true,后续的读者只能等待着写者先进,不会接连不断的进入。

聊了这么多锁,我们来看看死锁。

当一个程序拿着锁而另一个程序需要锁,此时需要锁的程序就会等着拿着锁的程序释放,但当二人各自拿着下一步对方的锁,就会互相等,就会产生死锁,就好像哲学家问题:

那什么情况会出现死锁?

当存在环时,就有死锁的可能性存在,此时大断死锁的方式可以直接 kill 全部 、kill 部分和回滚状态,回滚状态看起来是最好的,但是实现起来很难。

我们思索下该怎么防止死锁,假设我们让一个中间人来管理,所有的资源由他来分配:

第二种思路是,既然死锁的发生是你拿着锁等别人拿走的资源,那么让你不要拿着锁等待,要么锁全拿到,要么锁全无。

第三种思路,支持抢占,需要回滚,如线程A需要线程B的资源,就抢走,让线程B回退到拿到该资源前的状态:

但一般只用在简单、不复杂的情况下:

第四个方向就是调整拿锁的顺序,如上图,线程A是先拿A再拿B,线程B先拿B再拿A,如果我们调整下,让线程A、B的拿锁顺序都是资源A、B,这样线程A拿了锁后,线程B就只能等,不会出现死锁的情况:

通过银行家算法判断是否会发生问题:

银行家算法会根据每个进程的最大需求、已分配资源和系统剩余资源,判断这次分配后是否仍能找到一个安全序列,如果找不到安全序列,就拒绝或延迟本次资源分配

一种情况是活锁,多个线程都在运行,也都在主动让步或改变状态,但整体任务始终无法推进。就像不断在原地踏步一样:

还有一种情况是,高优先级想要锁,低优先级的拿着锁,这种情况叫优先级反转:

如果高优先级线程等待低优先级线程持有的锁,而中优先级线程又不断抢占低优先级线程,就会导致高优先级线程长期无法运行,这叫优先级反转。常见解决方法是优先级继承或优先级天花板协议。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇