但是线程切换就不一样了,之需要切换PC寄存器指向的代码地址就好,其他操作都不用做,所以线程切换的成本比进程切换低多了。
互斥和同步 简介当多个进程需要对同一个资源进行访问的时候, 为了避免同时使用这个资源造成的混乱, 所以需要考虑进程间的互斥。
典型的互斥实现方案如下:
方案介绍中断禁用 杀敌一千, 自损八百。虽然能实现互斥, 但是大大降低了处理器的执行效率。而且再多处理器体系结构中, 他还不能达到互斥
专用机器指令 往往是通过一个不可中断的指令, 用于原子修改内存中的值, 常见的两个指令是testset和exchange, 其对应的demo代码如下图。该方案的好处是实现简单, 坏处是使用了忙等待, 可能出现饥饿, 可能死锁, 需要操作系统层进行管理和避免
死锁的避免
死锁出现有4个必要条件:
互斥: 资源只能同时被一个进程使用
占有且等待: 一个进程在等待别的资源的时候, 将继续占有其拥有的资源
非抢占: 不能强行抢占别人占有的资源
循环等待: 在满足如上3个条件的情况下, 出现循环等待即出现死锁
要避免死锁也是从这4个条件上下手:
互斥: 这个是为了资源功能正常运转, 无法避免
占有且等待: 让进程一开始就申请所有资源才能运行。问题是效率低下, 进程可能要等待很长时间, 资源可能被控制很长时间, 程序也需要最开始就知道需要使用哪些资源;
非抢占: 根据进程优先级让申请资源的进程释放自己之前拥有的资源或者抢占别的进程的资源, 靠谱, 唯一的问题在于资源的使用不一定有那么容易的保存和恢复(很多硬件不像处理器那样可以随意切换使用进程的)
循环等待: 给资源定义一个序列, 进程只能按照序列申请资源, 会导致进程执行效率大大降低, 所以主流做法是如下两种
如上几种避免办法都有各种各样的问题, 所以一般不采用, 现在采用最多的方案还是从第4点出发, 只不过不预先避免, 而是动态探测:
如果一个进程启动或者新增资源需求会导致死锁, 则不允许此分配
典型的算法如银行家算法, 此方法的弊端是需要知道一个进程将来所需要占用的资源
允许所有请求, 周期性的进行检测死锁
动态检测, 运行效率高, 但是如果出现死锁频率比较高, 则系统运行效率会比较低。
综合所有的死锁避免的方法的对比如下:
编程界面多进程之间的互斥与同步方案有了如上提到的系列硬件支持之后, 就需要考虑操作系统对有并发编程的程序员们提供的编程界面了。
信号量信号量是在内存中维护了一个int, 每次操作对该int进行++或者--。
对操作者提供了两个接口:
semWait
该接口检查int值, 如果该值大于0, 就将该值--, 并进入临界区; 否则就阻塞检测该值知道大于0;
semSignal
该接口将int值++, 并通知受阻的所有进程。通知哪个进程有的采用FIFO策略, 有的采用随机策略。
管程信号量的方式比较灵活, 让程序员可以任意控制临界区以及交互设计, 大部分现在程序也都采用了类似的方案, 这是一个相对底层但是功能强大的方案。
但是有人提出了信号量的操作分散, 在模块中任何位置都可能出现, 造成程序编写和维护困难, 也容易出bug, 所以在70年代, 有人提出了管程的概念, 笔者在实际工作中尚未使用管程来实现进程间互斥和同步。
管程底层跟信号量类似, 只是他把所有加锁解锁的逻辑全部封装在一个类里面, 所有关于该临界资源的操作都在这个类中以函数的方式呈现, 除了这个类之外其他任何地方都看不到锁。这样就实现了锁相关逻辑集中在一个地方。
在一个类里面可以有多把锁, 跟信号量类似, 针对每把锁, 在该类的函数里可以用类似semWait和semSignal的接口等待该锁或者释放该锁。
消息传递消息传递的方式跟锁又有些不一样��, 因为进程间同步互斥不外乎就是阻塞和交换信息两类, 而消息传递提供的API就是最底层的API, 把其他逻辑都交给了上层由程序员控制。
其提供的API如下:
send(destination, message)
发送请求
receive(source, message)
接收请求
根据两个接口是否阻塞, 一般又分成如下几类:
send和receive都阻塞
一般用于进程间紧密同步的时候使用
send不阻塞, receive阻塞
比较常见的方式, send之后可以继续做别的事情, 但是receive这头在收到相关信息之前, 必须阻塞直到相关信息确认才能继续。
send和receive都不阻塞
比较少见。
一般现在在分布式系统涉及到跨机器写作的时候, 会使用该方式来做进程间的同步和互斥。