一片自留地 UCB cs162(操作系统) l5-l7 同步

magicyang · 2021年02月07日 · 1577 次阅读

前言

在写锁之前,先来说一个问题:

关于 thread 队列的调度

主要是要区分 thread.join 和 thread detach 的实现。
join:原创建线程会持有 thread,并且被创建 thread 结束之前,原线程的资源无法调度。这里 join 的动作需要连续,否则会变成串行操作。
detach:原创建线程不持有 thread,detach 后原线程继续执行。所以会出现子线程可能晚于主线程结束的情况。
关于 c++ 的线程池,可以参考:
https://www.zhihu.com/question/27908489
这里比 java 要复杂一些。关注:condition_variable 和 mutex 的使用。

操作系统还是很抽象很基础的,国外的课程偏实践,要真正理解不是容易的事情。

实现同步的相对简单的方式就是锁。
有问题的解决方案就不提了,这里只看正确的。

busy-wait



这里用了 busy-wait 的方式去等待,在 A 的 while 中会长期占用 cpu 资源,因此这个方案效能比较低。

中断实现:

有同步才有锁!如果数据、操作完全独立(纯并行)是不需要锁的。


用 stack 的实现举例,说明需要引入锁。

锁是通过等待实现的同步,等待是核心。

不推荐用硬件指令实现锁。

使能/禁止中断解决了部分问题,但也存在问题:

更好的方案:

其中 disable 中断的作用是在入队出队的时候,保持操作原子性,不会被其他线程打断。
入队出队的操作并不会花费很长的时间,不会导致其他资源得不到调度。

关于 sleep:
这里的 sleep 用一般理解的 sleep 是不行的。可能存在中断被屏蔽,sleep 又没唤醒,一直睡着的情况。
用 Interrupt Re-enable 的方案解决这个问题:

可以看到当线程 sleep 返回并得到调度后,重新激活中断。

通过几张图来看一下两个线程中断实现锁的流程:

Thread A 先调度,Thread B 进入 Waiting 状态

A 释放锁之后,A 依然是 RUNNING 态,B 进入 Ready 态。

然后在某个时钟周期,B 进入 Running 态,获取到锁资源,执行相关的操作

可以看到这个例子需要切换 thread 状态,需要内核操作,因此他的切换成本是比较高的,并不适合多机多核系统。

内存实现


不用系统调用,使硬件支持原子化的内存操作。因为操作的是内存,也就没有系统调用、内核切换的开销了。
同时对于多核多处理器系统,内存是共享的,是可以复用的。

使用内存进行原子操作

这里的问题在于,等待内存状态又需要长期等待。。。

Spin Locks:


相比于 test&set,wait 的时候只有读操作没有写操作,又性能的提升。
但是这依然是一种 busy-waiting 的解决方式,依然需要消耗大量的时间片,依然无法真正使用。

生产者消费者

第一个循环方案

这个方案的问题在于 while 可能会造成死锁。

改进方案

改进的方式是如果遇到可能会死锁的情况(while 语句中),释放一下锁,让其他线程有得到调度的机会。

信号量


从百度百科,找了个关于 p,v 的中文解释:

来看一下通过信号量实现的生产者消费者:

注意 p,v 的位置变化可能会导致死锁。

Monitor


Monitor 是一种并行编程范例,如 Java 本身就支持 Monitor,C 的 pthread 支持对应的操作。

关于条件变量的定义和操作定义。
还是参考课程的例子来看

注意这里没有队列长度的限制
同时关于一下 cond_wait 时,需要传入 mutex,可以参考:https://www.zhihu.com/question/24116967。不细说了。
Monitor 有 Mesa vs. Hoare 两种实现方式,其中 Mesa 是操作系统主要的实现方式:

生产者消费者基于 monitor 的实现:

基于生产者消费者,又举了一个 writer/reader 的例子。

总结


共收到 0 条回复 时间 点赞
magicyang 关闭了讨论 02月07日 16:23
magicyang 重新开启了讨论 02月07日 19:23
magicyang 关闭了讨论 02月08日 13:34
magicyang 重新开启了讨论 02月08日 18:16
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册