互斥锁
互斥锁是一种实现线程同步的机制:当一个线程尝试获取互斥锁,如果互斥锁已经被占用则该线程会被挂起进入睡眠状态,直到被唤醒。线程被挂起时,CPU会将该线程当前的处理状态保存到内存中,等到唤醒时从内存中读取上次的处理状态,这个CPU切换线程处理状态的过程被称为“上下文切换”。上下文切换是一个非常耗时的操作,它需要相当多的CPU指令才能完成。但在早期单核处理器中,只能通过这个方式来完成,毕竟一口锅不能同时炒两盘不一样的菜。
![3ee500285bb108cdf99262cd75789405.png](https://i-blog.csdnimg.cn/blog_migrate/070f389709ea313caf0797e44d40b603.jpeg)
自旋锁
多核处理器开始普及之后,使用互斥锁经常会出现一种尴尬的情况:一个线程因为尝试获取互斥锁失败而进入睡眠状态,但上下文切换还没完成或者说刚切换上下文没多久,另一个线程就已经释放了那个互斥锁(所以出现了自旋锁)。
![ec245d52ab03015d540de15d832493ab.png](https://i-blog.csdnimg.cn/blog_migrate/0a5815c77c6518d6eb2d6cecaf0ea65f.jpeg)
概念
自旋锁是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
自旋锁和互斥锁一样也是实现线程同步的一种机制
当一个线程尝试获取自旋锁时,如果自旋锁已经被占用则该线程会一直循环等待并反复检查锁是否可用,直到锁可用时才会退出循环。如果持有锁的线程很快就释放了并且线程竞争不激烈,那自旋的效率就非常好,反之,自旋就会白白浪费CPU的处理时间,这反而会带来性能上的损失。
问题
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- 不公平的锁(即无法满足等待时间最长的线程优先获取锁)就会存在”线程饥饿”问题。
- 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等
说明:我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快(非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换)
特点
- 单CPU非抢占内核下: 自旋锁会在编译时被忽略(因为单CPU且非抢占模式情况下,不可能发生进程切换,时钟只有一个进程处于临界区(自旋锁实际没什么用了)
- 单CPU抢占内核下: 自选锁仅仅当作一个设置抢占的开关(因为单CPU不可能有并发访问临界区的情况,禁止抢占就可以保证临街区唯一被拥有)
- 多CPU下: 此时才能完全发挥自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。
自旋锁与互斥锁
原理
- 互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
- 自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位。
用一句话概括互斥锁和自旋锁:互斥锁是睡等,自旋锁是忙等。
区别
性能问题
- 互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
释放问题
- 自旋锁你不需要操心锁持有进(线)程意外结束(加锁到解锁过程中进程被kill了)的时候,锁的释放问题(需要写点代码)
- 互斥锁最好用于那种生命周期特别长和特别稳定的代码段,例如中断处理例程内核代码。
其它(互斥锁看做二元信号量)
- 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因而自旋锁适合于保持时间非常短的情况。
- 自旋锁可以用于中断,不能用于进程上下文(会引起死锁)。而信号量不允许使用在中断中,而可以用于进程上下文。
- 自旋锁保持期间是抢占失效的,自旋锁被持有时,内核不能被抢占,而信号量和读写信号量保持期间是可以被抢占的。
应用
互斥锁(互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑)
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 被保护的共享资源只在进程上下文访问
- 临界区竞争非常激烈
- 单核处理器
自旋锁
- 主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。
被保护的共享资源需要在中断上下文访问(包括底半部(中断处理句柄)和顶半部(软中断))
总结
自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
既然互斥锁与自旋锁各有优劣,我们可以把它们结合到一起:当一个线程获取锁失败,先让它自旋一段时间,一段时间过后还未能获取锁,再让它进入睡眠状态。这个过程的重点在于自旋时间的长短,过长可能退化成单纯的自旋锁,过短可能退化成互斥锁。