大纲
- 常用锁介绍
- 自旋锁和互斥锁的一些问题
- NSLock及源码分析
- NSLock 坑
一、常用锁介绍
锁的目的是为了解决资源抢夺
iOS中的常用的锁有如下几种:
1、自旋锁:
使用与多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直到显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。NSSpinLock ,它现在被废弃了,不能使用了,它是有缺陷的,会造成死锁
2、互斥锁
是一种用于多线程编程中,防止两条线程同时对同一公共资源(例如:同一个全局变量)进行读写的机制。互相排斥。例如线程A获取到锁,在释放锁之前,其他线程都获取不到锁。互斥锁也分为两种:递归锁和非递归锁。互斥锁是通过将代码切片成一个一个的临时区来实现。p_thread_mutex,NSLock,@synchronized这个顺序是按照性能排序的,也是我们常用的几个互斥锁。
3、读写锁:
计算机程序的并发控制的一种同步机制,也称“共享 - 互斥锁”、多个读者,单个作者(写入)的锁机制。用于解决多线程对公共资源读写问题,读操作可并发重入,写操作时互斥的。读写锁通常用互斥锁、条件变量、信号量实现。
4、信号量:
是一种更高级的同步机制,有更多的取值空间。用来实现更加复杂的同步,而不单单是线程间互斥。semphone在一定程度也可以当互斥锁用,它适用于编程逻辑更复杂的场景,同时它也是除了自旋锁以外性能最高的锁
5、条件锁:
就是条件变量,当进程的某些资源要求不满足时就锁住进入休眠。当资源被分配到了,条件锁打开继续运行。NSCondition,条件锁我们调用wait方法就把当前线程进入等待状态,当调用了signal方法就可以让该线程继续执行,也可以调用broadcast广播方法。
临时区:
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
锁是线程编程同步工具的基础。iOS开发中常用的锁有如下几种:
- @synchronized
- NSLock 对象锁
- NSRecursiveLock 递归锁
- NSConditionLock 条件锁
- pthread_mutex 互斥锁(C语言)
- dispatch_semaphore 信号量实现加锁(GCD)
- OSSpinLock (暂不建议使用,原因参见这里)
二、自旋锁和互斥锁的一些问题
互斥锁
互斥锁又分 递归锁(NSRecursiveLock 等) 和 非递归锁(NSLock 等)。
递归锁:可重入锁,统一线程在锁释放前可再次获取锁,即可以递归调用
非递归锁:不可重入,必须等锁释放后才能再次获取锁。
自旋锁和互斥锁的区别?
互斥锁:当线程获取锁但没有获取到时,线程进入休眠状态。等到锁被释放,线程会被唤醒同时获取到锁。继续执行任务改变线程状态。
自旋锁:当线程获取锁没有获取到时,不会进入休眠,而是一直循环看是否可用。线程一直处于活跃状态,不会改变线程状态。
自旋锁和互斥锁的使用场景分别是?
自旋锁:由于自旋锁一直等待会消耗较多CPU 资源,但是效率较高一旦锁释放立刻就能执行无序唤醒。所以适用于短时间内的轻量级锁定。
互斥锁:需要修改线程状态,唤醒或休眠线程。所以适用于时间长相对自旋锁效率低的场景。
四、NSLock及源码分析
NSLock
非递归 互斥锁。NSLock 互斥锁 不能多次调用 lock方法,会造成死锁
遵循 NSLocking 协议。进行加锁、解锁
@protocol NSLocking
- (void)lock;//加锁
- (void)unlock;//解锁
@end
NSLock实现了NSLocking协议:
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
// 尝试获取锁,获取到返回YES,获取不到返回NO
- (BOOL)tryLock;
// 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 锁名称,如果使用锁出现异常,输出的log中会有
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
-tryLock:如果能获取到锁返回YES,如果获取不到锁返回NO,但不会使线程进入休眠,会继续向下执行
-lockBeforeDate::如果锁已被锁定,在指定的时间点之前线程进入休眠等待锁释放。如果在时间点之前锁被释放了,线程立即被唤醒获得锁,该函数会返回YES,继续执行任务,不会一直休眠等到那个时间点。如果等到时间点还没有获得锁会返回NO,并继续执行任务
NSLock是非递归锁,不能重入,否则会发生死锁:
#import "LJLNSLockViewController.h"
@interface LJLNSLockViewController ()
@property(nonatomic, strong) NSLock *lock;
@end
@implementation LJLNSLockViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.lock = [[NSLock alloc] init];
[NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];
}
//2020-04-06 13:49:43.713653+0800 filedome[74121:2725361] testLock1
- (void)testLock1 {
[self.lock lock];
NSLog(@"testLock1");
[self testLock2];
[self.lock unlock];
NSLog(@"testLock1: unlock");
}
- (void)testLock2 {
[self.lock lock];
NSLog(@"testLock2");
[self.lock unlock];
NSLog(@"testLock2: unlock");
}
可以看到上面的代码最终只打印了testLock1,其他的几个打印不会去执行。因为 testLock1被锁了之后,还没有调用解锁就执行了testLock2。这个时候去lock 但是锁获取不到就休眠等待,直到testLock1 unlock解锁之后才会继续执行,但是这个时候testLock2 不执行完, testLock1 里面的代码也就被卡着不能继续。
注意:
-lock和-unlock必须在相同的线程调用,也就是说,他们必须在同一个线程中成对调用,否则会产生未知结果。
官方文档原文:Unlocking a lock from a different thread can result in undefined behavior.
在不断循环递归,多线程操作的时候。这个时候 _testArray 不断的去初始化新增,release 旧值 如果多条线程访同时敢问到release 那么就会造成多次释放这时候就需要禁止重入的互斥锁。例如:
NSLock * lock = [[NSLock alloc] init];
for (int i=0; i<200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
_testArray = [NSMutableArray array];
[lock unlock];
});
}
NSLock 源码
NSLock是在Foundation中实现的,开源的Foundation是Swift版的:
源码 目录:demos->003-锁分析->2-Foundation源码
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
}
// 对应OC中的 -tryLock
open func `try`() -> Bool {
return pthread_mutex_trylock(mutex) == 0
}
// 对应OC中的 -lockBeforeDate:
open func lock(before limit: Date) -> Bool {
if pthread_mutex_trylock(mutex) == 0 {
return true
}
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
}
private func timedLock(mutex: _MutexPointer, endTime: Date,
using timeoutCond: _ConditionVariablePointer,
with timeoutMutex: _MutexPointer) -> Bool {
var timeSpec = timeSpecFrom(date: endTime)
while var ts = timeSpec {
let lockval = pthread_mutex_lock(timeoutMutex)
precondition(lockval == 0)
let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)
precondition(waitval == 0 || waitval == ETIMEDOUT)
let unlockval = pthread_mutex_unlock(timeoutMutex)
precondition(unlockval == 0)
if waitval == ETIMEDOUT {
return false
}
let tryval = pthread_mutex_trylock(mutex)
precondition(tryval == 0 || tryval == EBUSY)
if tryval == 0 { // The lock was obtained.
return true
}
// pthread_cond_timedwait didn't timeout so wait some more.
timeSpec = timeSpecFrom(date: endTime)
}
return false
}
通过源码可知验证 NSLock 是对 pthread 中互斥锁 的封装。
其他都好理解,这里列一下 timedLock() 的实现流程:
1、设定超时时间,进入while循环。
2、pthread_cond_timedwait()在本次循环中计时等待,线程进入休眠
3、等待超时,直接返回 false;
4、如果等待没有超时,期间锁被释放,线程会被唤醒,再次尝试获取锁 pthread_mutex_trylock(),如果获取成功返回true
5、即没有超时,被唤醒后也没有成功获取到锁(被其他线程抢先获得锁),重新计算超时时间进入下一次while循环
五、NSLock 坑
如下代码。递归调用 testMethod (方法)输出,正常应该是10 9 8 .....,但是实际只输出了10.
原因分析:因为在if 之前进行锁了之后,在if 里面有递归调用了 testMethod 方法,又一次进来有锁了,这样被锁了多次都没去执行解锁一直处理阻塞。
这里应该换成递归锁.
例如:NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
NSLock * lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void(^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value >0) {
NSLog(@"current value = %d",value);
testMethod(value-1);
}
[lock unlock];
};
testMethod(10);
});