毕业以后,操作系统知识已经丢的差不多了,最近在工作中遇到了一些线程同步、异步访问控制等问题,Google了目前在技术论坛上的关于锁的文章,大多数都是灌水和Copy的一些内容,甚至还有一些文章在概念性错误,所以在此对锁的学习进行一些记录和总结。
什么是锁
多线程中,对共享资源进行访问,为了防止并发引起的相关问题,通常都是引入锁的机制来处理并发问题。学术上对线程锁有好几种不同的定义方式,这里要对锁的几个概念做一个解释。
临界区
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
阻塞锁和非阻塞锁
阻塞锁和非阻塞锁的区别,线程访问临界区时,该资源上锁与否线程是否被挂起。阻塞锁会挂起线程,等待临界区解锁,而非阻塞锁会保持活跃状态。
递归锁和非递归锁
递归锁和非递归锁的区别,当一个线程多次获取同一个递归锁时,线程不会产生死锁。但是一个线程多次获取同一个非递归锁,则会产生死锁。从效率层面上来说,非递归锁的效率高于递归锁
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
自旋锁
非阻塞锁
非递归锁
自旋锁(Spin Lock),它的工作原理是当某个线程需要访问临界区时,如果该临界区已经被上锁,那么该线程不会被挂起,而是会循环请求线程锁,此时线程处于忙等的状态(在非耗时操作下,这种忙等是可以接受的),直到该资源被解锁释放。
线程挂起主动出让时间片的做法是有性能消耗的,这种上下文切换会通常占用10μs。所以非阻塞锁是性能最高的锁。
iOS系统下可用的自旋锁:
OSSpinlock
:iOS10以后被废弃,有可能造成死锁,参考ibireme的文章,不再安全的 OSSpinLockos_unfair_lock
:iOS10之后支持,解决了OSSpinlock优先级反转的问题。从底层看线程处于休眠状态,并非处于忙等,该锁实现原理有待考证
1 |
|
互斥锁
阻塞锁
递归锁
非递归锁
互斥锁(Mutex),它的工作原理是当某个线程访问临界区已经被加锁,那么该线程会进入休眠状态。当临界区解锁,则等待线程会被唤醒。互斥锁要保证在任一时刻,只能有一个线程访问临界区,同时只有上锁线程能够进行unLock操作
iOS系统下可用的互斥锁:
pthread_mutex
:C语言实现的互斥锁,可以指定是否是递归锁,效率高。Foundation框架下实现的锁基本都是基于它封装的
1 |
|
NSLock
:OC对象封装的非递归锁
1 |
|
NSRecursiveLock
:OC对象封装的递归锁
1 |
|
@synchronized
:牺牲了效率换来语法上简洁的互斥锁,非递归锁
1 | { |
条件锁
阻塞锁
非递归锁
条件锁(Condition Lock),实际上是对一个互斥锁和一个条件变量的封装。当线程想要访问临界区,需要满足Condition
iOS系统下可用的互斥锁:
- NSCondition
- NSConditionLock
信号量
信号量(Semaphore)是实现异步调度的一种策略,这种机制可以实现线程加锁的目的。信号量机制与互斥锁最大的区别,是互斥锁要保证统一时间只能有一个线程访问临界区,但是信号量可以任意指定同时访问临界区的线程数
iOS在GCD中封装了dispatch_semaphore
,用于实现信号量调度
dispatch_semaphore_create(long value)
:
初始化dispatch_semaphore_t
类型的信号量,参数value是最大并发量。注意value须大于0,否则会返回null。dispatch_semaphore_signal(dispatch_semaphore_t signal)
:
参数signal是传入所需信号量,并使传入的信号量加1,可以理解为解锁。dispatch_semaphore_wait(dispatch_semaphore_t signal, dispatch_time_t timeout)
参数是传入一个信号量和一个超时时间。当传入的信号量的值大于0(可执行并发),会继续执行临界区代码,并且将传入的信号量减1。当传入的信号量的值等于0(无可并发资源),则线程进入休眠状态主动让出时间片,并将该临界区任务加入等待队列,待信号量加1时,执行队列顶部任务。如果在线程休眠的过程中一直没有收到信号直到timeOut,则线程会继续访问临界区。可以理解为加锁。
使用代码如下:
1 | { |
总结
- 具体使用哪一种锁,要根据不同的业务场景和功能性需求进行选择
- 在保证没有递归获取并且线程优先级一致的情况下,临界区非耗时操作可以选择自旋锁
- 如果不能保证访问临界区线程优先级相同,并且要求对数据的原子性操作,那么推荐使用互斥锁,这里建议尽量使用非递归锁,首先是效率上较高并且在发生死锁的时候容易Debug
- 如果想要控制最大并发,允许多线程访问临界区,可以使用信号量控制
- 推荐重点学习掌握
pthread_mutex
和dispatch_semaphore