windows临界区和互斥量效率_互斥那点儿事

本年度第 10 次操作系统成员会议开始啦!

一月一度的会议旨在让大家互相交流,解决最近在工作中出现的问题,以提高整个计算机系统的工作效率。因为计算机硬件在飞速发展,而操作系统是连接计算机硬件和应用程序的中间层,如果故步自封,很快就会被市场淘汰,所以每位操作系统成员都很重视月度会议。

这次提出问题的是进程和线程两兄弟。

站在众人前面,线程显得有些怯场,他戳了戳进程,示意让他先来讲。进程迅速整理了下思路,挺直了身板,说:“这次的问题是在一个订票系统里发现的,我把这个系统的简单逻辑画出来了,你们一边看我一边说。”

59883c094184d18454a5db9c1714e379.png

“这个订票系统分为服务器端(server)和客户端(client),当用户与服务器建立连接时,服务器端就会建立一个新的线程来为客户端提供服务。订票逻辑是这样的:

701aeaf85e940335d15912bea8abf9a4.png

单独从这个逻辑图上看是没有问题的,但在实际情况下,因为经常出现多个用户同时抢订一张票的情景,这种方式就可能会出错。就像这样:

a5fcc2308ed3dad7f68611b90d9c73e8.png

在线程 A 确定完余票(假设是 1),但还未能成功订票之前,线程 B 得到了余票数为 1 的信息,所以 B 也认为可以订票,最后导致一张票卖出去两份。“

内存一针见血的道:“我看这就是几个线程执行流的冲突问题嘛,本来应该一个线程订票操作结束后,另一个线程才能查询余票。像这样执行流交叉,肯定还会出现其它意想不到的问题。”

进程佩服的说:“诶别说,内存你说的太有道理了,我也遇到过类似的情况,上次我和另一个进程共享一部分内存空间,结果在使用同一个数据的时候,他把我刚写进去的数据覆盖掉了,害得我后面的计算全出错了。”

这时,磁盘发表了他的看法:“执行流的问题,那一看就是进程调度器的锅,怎么非得在别人执行到关键步骤的时候把人家从 CPU 上赶下来!要是调度器稍微等一会儿,这问题不就解决了?”

进程调度器听到这话,气的站起来,说:“你,你怎么凭空污人清白!什么时候切换进程不是由我来决定好不好?我是负责从就绪队列选出最应该使用 CPU 的进程而已。等我开始调度的时候,那些进程就已经被操作系统撤下来了。”

操作系统补充道:“调度器说的没错,调度的时机是由中断决定的。看样子这种情况出现在进程时间片用尽的时候,出现了时钟中断,然后被其他进程抢占了 CPU 资源。”

磁盘听了,不好意思的说:“对不起,刚刚是我太武断了。那照你的意思,我们在执行到这部分代码的时候,像这样屏蔽时钟中断可以解决这个问题了?”

c10530dbea715695e038a624e9a7e7ef.png

操作系统摇摇头:“「中断禁用」这种方式确实可以防止进程在运行这部分代码时进行切换,但是,时钟中断是我的一项非常重要的功能,怎么能随随便便就把控制权交给人类呢?万一有的程序员想要他们的代码可以完全占有 CPU ,不把时钟中断给我开启怎么办?我是不可能把这种重要权限交出去的,我要对整个系统负责。”

内存在旁边赞同道:“除了这一方面,你还要知道,现在都是多核时代了,你即使禁用了这个 CPU 的时钟中断,其他几个核还是能切换进程,然后访问这些数据。磁盘啊,你明明存了那么多文件,怎么懂得还是那么少。。。”

磁盘愤愤的道:“别瞧不起我,我这就去找有没有办法解决这个问题!”

思考了许久的 CPU 开口了:“我来捋一捋吧,现在咱的目标是,不让两个进程同时执行这一段代码——我们把这段代码叫做临界区吧,换句话说,我们需要让进程互斥的进入临界区。那我们就把这段临界区「加锁」,”

“加锁?这是什么意思?”

“加锁是个比喻,其实「」只是一个共享变量,我们可以让它有 OPENCLOSE 这两个值。一个进程,比如说 A,进入临界区之前,先检查锁是不是 OPEN 状态,如果是的话,就把锁改为 CLOSE 状态 ,这样其他进程在进入临界区时,会发现锁已经 CLOSE 了,那就让他们循环等待 ,直到 A 出临界区然后将锁打开。”

内存眉头一皱,发现事情并没有这么简单——如果 A 发现锁是开着的,但在 A 还没有关闭锁之前,切换到了进程 B ,那么 B 也会发现锁是开着的,那么 B 也将能够进入临界区

想到这里,内存把问题告诉 CPU,但 CPU 说,这对他不是问题。

原来计算机里有一条硬件支持的指令——TSL(test and set lock,测试并加锁),这条指令可以保证读字和写字的操作「不可分割」,也就是说,在这条指令结束前,就连其他处理器也不可能访问该内存字。

“TSL 指令会把内存字 lock 读到寄存器上,然后在对应的内存地址上写入一个非零值。那我们就可以利用这条指令改进刚刚的加锁的方法,就像这样:

dd204598f9d4f103b69e6e00d93def33.png

我们让进程在进入临界区之前先调用 enter_region ,如果锁已经被关闭(表现为锁非 0 ),就循环调用enter_region ,直到锁打开,然后再进入临界区。出临界区之后,就调用 leave_region 把锁打开。这样不就解决你的问题了?“

内存点点头,说:“这确实是一个好方法,解决了临界区的互斥问题。”

不过操作系统不是很满意这种解决方案:“这种解决方式需要忙等待,浪费了 CPU 的资源啊,我觉得这种 TSL 方案需要改进。”

这时候大家陷入了沉默——谁也没有想到更好的解决方案,会议好像就此僵住了。

“我找到好办法了!”

没有想到,说话的人竟然是磁盘!

进程调度器瑟瑟的说:“你有方法?还是算了吧,我怕用你的方法操作系统要乱套了。”

磁盘委屈的道:“不就是刚刚冤枉你了吗,这么小气干什么!再说了,这个方法不是我想出来的,是我从文件里找到的。”

操作系统挑了挑眉毛:“哦?你找到什么文件了,让大家也瞅瞅?”

磁盘嗡嗡的转起来,很快就把文件取出来了。

“当当当当~ 这可是大师 Dijkstra 的论文,他引入了一个全新的变量类型——信号量(semaphore)。然后还为信号量设置了两种操作,P(proberen,检测) 和 V(verhogen,增量) 。”

”说清楚点啊,信号量是怎么个用法啊?“进程急切的问道。

“别急,让我接着看。。。Dijkstra 提出,P操作是检测信号量是否为正值,如果不是,就阻塞调用进程。 V操作能唤醒一个阻塞进程,让他恢复执行 。具体点的话就是这样: “

// S 为信号量
P(s):
{
S = S - 1
if (S < 0)
    {
        调用该 P 操作的进程阻塞,并插入相应的阻塞队列;
    }
}
// S 为信号量
V(s):
{
S = S + 1
if (S <= 0)
    {
        从等待信号量 S 的阻塞队列里唤醒一个进程;
    }
}

内存仔细看了代码,说:”这个实现也要求是原子操作诶,Dijkstra 这个方法很有趣啊。“

进程蒙圈了:“我怎么完全看不懂啊?内存你给我讲讲呗。”

“好,我就用最简单的一组线程举例子了:

// 线程 A,B,C , S = 1
...
P(S)        //S = S - 1  若 S < 0 ,阻塞等待
购票操作
V(S)        //S = S + 1  若 S <= 0, 表明有线程阻塞了,得唤醒其中一个 
...

这里的 「购票操作」 就是我们要保护的临界区,我们要保证一次只能有一个线程进入。那我们就把 S 的初始值设为 1 。当线程 A 第一个调用 P(S) 后,S 的值就变成了 0 ,A 成功进入临界区。在 A 出临界区之前,线程 B 如果调用 P(S), S 就变成 -1 ,满足 S < 0 的判断条件,线程 B 就被阻塞了。等 A 调用 V(S) 后,S 的值又变成 0 ,满足 S <= 0,就会把线程 B 唤醒,B 就能进入临界区了。“

进程恍然大悟:“原来是这样,看起来和二元锁差不多啊,但是不用忙等待了。”

内存神秘一笑:“信号量能做的可不止这些,你想想看,要是我把 S 的初始值设为 2 ,会发生什么?”

“一次能有两个线程访问临界区!”进程这次反应快多了:“也就是说 S 的初始值可以控制有多少个线程进入临界区,太厉害了!”

tobe 注:从信号量的值能看出还有多少个进程能进入临界区,如果为负数,表明有 x 个进程因为调用 P(S) 而被阻塞

“没错,所以说信号量是一个很灵活的并发机制。而且信号量还有另一个厉害的用处:

fb0a63e77d1be1915b73c2a50645e93a.png

你看这两个进程有什么特别的地方?“

“emmmm,这个嘛,进程 P2 的 V 操作居然放在 P 操作的前面,而且两个操作的信号量还不是同一个。”

“没错,这样使用信号量,能让两个进程做到同步。你看,如果 P1 运行到 P(S1),他是不是会阻塞?”

进程认真一看,说:“没错诶,S1 初始值是 0,P1 肯定得停在这一句。让我再看看,,,如果 P1 想接着运行,就得等 P2 调用 V(S1) 把他唤醒。”

“是的,这就是同步——运行快的 P1 必须在这里停下来等 P2 运行到指定位置。两个进程的执行顺序就是这样:

c3cf9a981c910072560afa52510213af.png

也就是说 x 最终的值必然是 30,而不可能是 20。在信号量的帮助下,这两个进程达成了同步。“

进程由衷的感叹:“信号量实在是太强大了!咱们以后就用信号量来解决互斥的问题吧!”


在 Linux 里提供了信号量互斥量(也就是二元锁)这两种机制实现互斥,不过 Linux 的信号量功能要比文章里讲得复杂得多,「UNIX 环境高级编程」这本书里写到「。。。三种特性造成了这种并非必要的复杂性」,对于一般的互斥操作,还是建议使用互斥锁(当然是阻塞而非忙等待)。稍微复杂点的锁还有「读写锁」,以后有机会再讲吧~

觉得我写的还不错的话,就点个赞吧!

PS:如果你有什么建议,欢迎评论~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值