操作系统——常见并发问题

常见并发问题

非死锁缺陷

违反原子性

  • 即代码段本意是原子的,但是在执行中并没有强制实现原子性
  • 解决方法:给有需要的代码加锁

违反顺序缺陷

  • 两个内存访问的预期顺序被打破了(即A应该在B之前执行,但是实际运行中却不是这个顺序)
  • 解决方法:通过加锁、条件变量以及状态变量配合来强制顺序执行

死锁缺陷

为什么发生死锁

  • 大型代码库里,组件之间会有复杂的依赖关系,必须仔细地避免循环依赖导致的死锁
  • 模块化封装,使得某些看起来没有关系的接口可能会导致死锁

产生死锁的条件

  • 互斥:线程对于需要的资源的进行互斥的访问
  • 持有并等待:线程持有了资源,同时又在等待其他资源
  • 非抢占:线程获得的资源,不能被抢占
  • 循环等待:线程之间存在一个环路,环路上每个线程都额外持有一个资源,而这个资源又是下一个线程要申请的

预防死锁

  • 循环等待

    • 最使用的预防技术就是让代码不会产生循环等待,最直接的方法就是获取锁时提供一个“全序”,即针对锁A、B,每次加锁都严格先申请A、后申请B的顺序进行。
    • 复杂系统锁很多时,全序可能很难做到,“偏序”可能是一种有用的方法,安排一部分锁的加锁顺序
    • 缺点:全序和偏序都需要细致的锁策略的设计和实现,并且顺序只是一种约定,很容易被忽略而导致死锁
    • tips:通过锁的地址来强制锁的顺序,按地址从高到低或者从低到高的顺序来加锁
  • 持有并等待

    • 死锁的持有并等待条件,可以通过原子地枪锁来避免。即先抢占全局锁,由它来保证枪锁过程中不会有不合时宜的线程切换

      • lock(prevention);
        lock(L1);
        lock(L2);
        ...
        unlock(prevention);
        
    • 缺点:它不适用于封装,因为这个方案需要我们准确地知道要抢哪些锁,并且提前抢到这些锁。因为要提前抢到所有锁(同时),而不是在真正需要的时候,所以可能降低了并发

  • 非抢占

    • 使用trylock()函数尝试获得锁,或者返回-1表示锁已经被占有

      • top:
            lock(L1);
            if (trylock(L2) == -1) 
            {
                unlock(L1);
                goto top;
            }
        
    • 新问题,可能会导致“活锁”,两个线程重复trylock()但又无法枪锁成功,解决办法是循环结束时随即等待一个时间后再重复整个动作

    • 缺点:封装不友好,如果某个锁封装在函数内部,那么跳回开始处很难实现;如果枪锁失败返回top重新枪锁,需要释放资源

  • 互斥

    • 最后的预防方法是完全避免互斥,通常来说,代码都会存在临界区,因此很难避免互斥。主要想法是:通过强大的硬件指令,可以构造出不需要锁的数据结构,如“比较并交换”指令等

通过调度避免死锁

  • 除了死锁预防,某些场景更适合死锁避免,我们需要了解全局的信息,包括不同线程在运行中对锁的需求情况,从而使得后续的调度能够避免产生死锁
  • 缺点:应用场景很局限,条件严苛;会限制并发,影响性能

检查和恢复

  • 常用策略是允许死锁偶尔发生,检查到死锁时再采取行动。如果死锁很少见,这会是很实用的方法
  • TOM WEAT定律:不要总是完美,不是所有值得做的事情都值得做好,如果坏事很少发生,并且造成的影响很小,那么我们不应该去花费大量的精力去预防它。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值