今天简单写一下iOS中相关锁的内容,下图来自不再安全的 OSSpinLock中几种常见的锁加解锁的时间。
以下为我自己测试的结果,加上了iOS10以后的 os_unfair_lock_lock
OSSpinLock: 333.91 ms
os_unfair_lock_lock: 420.40 ms
dispatch_semaphore: 374.38 ms
pthread_mutex: 459.84 ms
NSCondition: 465.79 ms
NSLock: 470.10 ms
pthread_mutex(recursive): 800.02 ms
NSRecursiveLock: 712.30 ms
//多次测试NSRecursiveLock与pthread_mutex的递归锁各有先后,水平有限不知道为啥
//还请有知道的大神不吝赐教。
NSConditionLock: 1581.54 ms
@synchronized: 1980.12 ms
---- 加解锁 (10000000)次 测试设备 6s-iOS11.4 ----
废弃的OSSpinLock
OSSpinLock(自旋锁)是属于busy-waiting类型的锁,与互斥锁不同,当SpinLock被其它线程持有,spinLock不会被阻塞,而会一直的请求获取lock,从而消耗大量cpu资源。所以当临界区任务时间较长时,并不适合用SpinLock。但当任务时间较短,其效率贼高。
另外开头已经提到 OSSpinLock之所以不再使用,是因为当低优先级的线程获得了锁,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU,此时低优先级线程无法与高优先级线程争夺 CPU 时间,导致任务无法完成。这就是优先级反转。
时间片轮转算法,每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。
os_unfair_lock_t
os_unfair_lock_t是官方推荐的替代OSSpinLock的方案,优化了优先级反转问题。
os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);;
os_unfair_lock_lock(lock);
os_unfair_lock_unlock(lock);
dispatch_semaphore
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来使用。在没有等待情况出现时,它的性能比 pthread_mutex 还要高。但一旦有等待情况出现时,会线程进入睡眠状态,主动让出时间片,让出时间片会导致操作系统切换到另一个线程,这就是所谓的上下文切换,通常需要 10 微秒左右,而且至少需要两次切换。如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。
关于dispatch_semaphore在深入理解GCD中写的也很详细。
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(lock);
NSLock & pthread_mutex
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。
pthread_mutex 表示互斥锁。互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
NSLock底层也是使用pthread_mutex实现,属性为PTHREAD_MUTEX_ERRORCHECK,因为多了方法发送等流程,多次调用后因为方法缓存两者的差距很小。
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
NSLock *lock = [NSLock new];
[lock lock];
[lock unlock];
pthread_mutex(recursive)& NSRecursiveLock
NSRecursiveLock与NSLock类似,也是使用pthread_mutex实现,只是类型为 PTHREAD_MUTEX_RECURSIVE。
一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。
然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己,由此也就引出了递归锁:允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。
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_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
//do work
pthread_mutex_unlock(&lock);
NSCondition
NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者。所以与NSLock基本类似,性能也很接近。
NSCondition *lock = [NSCondition new];
[lock lock];
//do work
[lock unlock];
NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值。
它的 lockWhenCondition 方法其实就是消费者方法:
- (void) lockWhenCondition: (NSInteger)value {
[_condition lock];
while (value != _condition_value) {
[_condition wait];
}
}
对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了所有的消费者:
- (void) unlockWithCondition: (NSInteger)value {
_condition_value = value;
[_condition broadcast];
[_condition unlock];
}
@synchronized
从上图可以看出@synchronized 性能是最差,语法最为简单
每个调用 sychronized 的对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。
具体实现可以看这关于 @synchronized,这儿比你想知道的还要多
@synchronized(object) {
//do work
}