8月8号操作系统学习感悟
怎么避免死锁
死锁的概念
在多线程编程中,我们会在操作共享资源之前加上互斥锁,只有获得了锁的线程才能操作共享资源,其余线程就只能等待线程的释放。因此,当两个线程分别对两个不同的共享资源加锁而又希望获得对方已经加锁的资源时,两个线程就可能一直等待对方释放锁,这种情况就就是发生了死锁。
死锁满足的四个条件
互斥条件(即一个资源不能被多个线程同时操作);持有并等待条件(即线程在等待期间不会主动释放已获得的资源);不可剥夺条件(即当一个线程获得了一个资源的拥有权之后,其余线程不能剥夺其拥有权);环路等待条件(即等待链是环形没有出口的)
破坏死锁
因此,要破坏死锁只需要破坏死锁的四个条件之一即可。
锁的类型
最基本的两种锁:互斥锁和自旋锁
- 互斥锁:当获取不到锁时(加锁失败后)进入锁的等待队列,将CPU交给其他线程执行。
- 自旋锁:当获取不到锁时,线程就会一直忙等待,不做任何事知道它拿到锁。
互斥锁
对于互斥锁,当线程释放CPU时,它的加锁代码就会被阻塞,而这种阻塞的实现,是由操作系统内核完成的。当加锁失败时,内核会将线程置为睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程。
因此,使用互斥锁会有一定的性能开销成本,因为会有两次线程上下文的切换。因此我们必须考虑被互斥锁锁住的代码执行时间是否远远大于这部分切换时间(比如代码只执行1微秒,而线程切换两次需要5微秒),否则应该考虑使用自旋锁。
自旋锁
自旋锁是由两个步骤合成的一条原子指令,两个步骤分别是“第一步:检查锁的状态,如果锁是未被占有的,则执行第二步;第二步:将锁设置为当前线程持有。”。使用自旋锁不会发生线程上下文切换,因此开销要比互斥锁小一些。
但是在单核CPU上使用自旋锁需要考虑到更多的细节,因为一个自旋的线程永远不会放弃使用CPU。
读写锁
读写锁人如其名是由读锁和写锁两部分组成的。
- 当写锁没有被持有时,所有线程都能并发地持有读锁,因为读操作并不会破坏资源的数据,因此这是线程安全的。
- 当写锁被持有,所有的读操作都会被阻塞(因为此时读到的数据可能是不准确的),并且在写锁被释放之前不能被其他线程持有。
由此可以看出,读写锁适用于读操作大于写操作的情景。
读写锁需要解决的第二个问题是如何处理写锁持有权的问题
主流的有三种解决方案:
- 读优先锁:只要有线程持有读锁,那么写锁的获取操作就会被阻塞,并且在阻塞的过程中,其他读线程仍然可以获取读锁,直到所有读锁被释放后,被阻塞的操作才会被恢复。
- 写优先锁:假使当前仍有线程持有读锁,那么获取写锁的操作也会被阻塞,但是后续其他线程将不能获取到读锁,它们都将被阻塞并排队在获取写锁操作的后面,当最开始的读锁释放后,写锁将被获取,当写锁被释放后,其余线程才能获取读锁。
- 因为以上两种策略都会引起线程饥饿问题,因此还有一种更公平的解决方案:”公平读写锁“。方法是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现线程饥饿的现象。个人感觉这不就是第二种解决方案吗?还是说写操作之前的所有读操作也需要排队吗,那不就违背读写锁设计的初衷了吗。。。
乐观锁和悲观锁
前面所提到的所有锁都属于悲观锁。
- 悲观锁认为:多线程修改共享资源发生冲突的概率比较高,所以访问共享资源前,需要加锁。
- 乐观锁认为:先假定多线程修改共享资源发生冲突的概率比较低,先修改完共享资源,再验证所有修改有没有发生冲突,如果没有冲突或没有其他线程在修改资源,那么操作完成;如果发生冲突,则放弃本次操作或由程序员手动解决冲突。因此,乐观锁不需要加锁。
再来说一说乐观锁的应用场景:比如腾讯在线文档,代码协同开发管理工具都是采用乐观锁。在一个人修改腾讯在线文档时,其他人也能修改文档并且还会有什么人在修改第几行的友好提示;像SVN,码云等如果多个程序员对整个项目代码的修改发生了不能合并的冲突,那么程序员可以手动解决冲突。
总结:不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,即加锁的粒度要小,这样执行速度会比较快。再者,锁的选用也很重要,如果使用了合适的锁,那么代码执行就会更快。
本文来自博客园,作者:Niwde,转载请注明原文链接:https://www.cnblogs.com/Niwde/p/16562835.html

浙公网安备 33010602011771号