什么是锁
多线程中,对共享资源进行访问,为了防止并发引起的相关问题,通常都是引入锁的机制来处理并发问题。学术上对线程锁有好几种不同的定义方式,这里要对锁的几个概念做一个解释。
-
临界区
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
-
阻塞锁和非阻塞锁
阻塞锁和非阻塞锁的区别,线程访问临界区时,该资源上锁与否线程是否被挂起。阻塞锁会挂起线程,等待临界区解锁,而非阻塞锁会保持活跃状态。
-
递归锁和非递归锁
递归锁和非递归锁的区别,当一个线程多次获取同一个递归锁时,线程不会产生死锁。但是一个线程多次获取同一个非递归锁,则会产生死锁。从效率层面上来说,非递归锁的效率高于递归锁
-
死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
自旋锁
非阻塞锁
非递归锁
自旋锁(Spin Lock),它的工作原理是当某个线程需要访问临界区时,如果该临界区已经被上锁,那么该线程不会被挂起,而是会循环请求线程锁,此时线程处于忙等的状态(在非耗时操作下,这种忙等是可以接受的),直到该资源被解锁释放。
线程挂起主动出让时间片的做法是有性能消耗的,这种上下文切换会通常占用10μs。所以非阻塞锁是性能最高的锁。
iOS系统下可用的自旋锁:
-
OSSpinlock
:iOS10以后被废弃,有可能造成死锁,参考ibireme的文章,不再安全的 OSSpinLock -
os_unfair_lock
:iOS10之后支持,解决了OSSpinlock优先级反转的问题。从底层看线程处于休眠状态,并非处于忙等,该锁实现原理有待考证
#import <os/lock.h>
{
os_unfair_lock_t unfairLock;
unfairLock = &OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(unfairLock);
...
os_unfair_lock_unlock(unfairLock);
}
互斥锁
阻塞锁
递归锁
非递归锁
互斥锁(Mutex),它的工作原理是当某个线程访问临界区已经被加锁,那么该线程会进入休眠状态。当临界区解锁,则等待线程会被唤醒。互斥锁要保证在任一时刻,只能有一个线程访问临界区,同时只有上锁线程能够进行unLock操作
iOS系统下可用的互斥锁:
-
pthread_mutex
:C语言实现的互斥锁,可以指定是否是递归锁,效率高。Foundation框架下实现的锁基本都是基于它封装的
#import <pthread.h>
/*
* PTHREAD_MUTEX_NORMAL 默认非递归锁
* PTHREAD_MUTEX_ERRORCHECK 非递归锁
* PTHREAD_MUTEX_RECURSIVE 递归锁
* PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
*/
{
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// 指定互斥锁类型为非递归锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutex_lock(&lock);
...
pthread_mutex_unlock(&lock);
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&lock);
}
-
NSLock
:OC对象封装的非递归锁
#import <Foundation/Foundation.h>
{
NSLock *lock = [[NSLock alloc] init];
[lock lock];
...
[lock unlock];
}
-
NSRecursiveLock
:OC对象封装的递归锁
#import <Foundation/Foundation.h>
{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
[lock lock];
...
[lock unlock];
}
-
@synchronized
:牺牲了效率换来语法上简洁的互斥锁,递归锁
{
@synchronized (NSObject.new) {
...
}
}
条件锁
阻塞锁
非递归锁
条件锁(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,则线程会继续访问临界区。可以理解为加锁。
使用代码如下:
{
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(lock);
}
总结
- 具体使用哪一种锁,要根据不同的业务场景和功能性需求进行选择
- 在保证没有递归获取并且线程优先级一致的情况下,临界区非耗时操作可以选择自旋锁
- 如果不能保证访问临界区线程优先级相同,并且要求对数据的原子性操作,那么推荐使用互斥锁,这里建议尽量使用非递归锁,首先是效率上较高并且在发生死锁的时候容易Debug
- 如果想要控制最大并发,允许多线程访问临界区,可以使用信号量控制
- 推荐重点学习掌握
pthread_mutex
和dispatch_semaphore