前言
在写锁之前,先来说一个问题:
关于 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 的例子。
总结