iOS多线程中的锁
涉及到多线程共享资源的情况下就避免不了资源竞争的问题,这时候就会用到各种锁,例如,@synchronized
、NSLock
、OSSpinLock
等等,虽然都是锁,但是锁的底层实现也不尽相同。大概可以分为:互斥锁、自旋锁、递归锁、
读写锁。
{
lock()//加锁
doSomeThing()//临界区
unlock()//解锁
}
我们锁的目的是在任何时刻最多只能有一个线程在修改资源,即在任何时刻只能有一个操作者保持锁。
互斥锁
通过一个全局变量来控制某个线程是否可以在某个时刻访问资源,该变量值大于0说明锁正在被使用。如果某个线程在尝试获得锁的是否发现已经被占用,线程挂起,等待锁的释放。
自旋锁
通过一个全局变量来控制某个线程是否可以在某个时刻访问资源,该变量值大于0说明锁正在被使用。如果某个线程在尝试获得锁的是否发现已经被占用,进入忙等状态,直到获得锁。
以上可以看出,互斥锁和自旋锁很类似,稍有不同的地方在于当处于等待状态的时候各自的线程是怎么调度的。互斥锁即使时间片还未用尽,互斥锁也会直接挂起,这样就减少了CPU的开销。但是这时候会进行上下文(线程)的切换,也会带来一定的开销,适合I/O密集型操作。自旋锁,顾名思义,即使处于等待状态线程也不会挂起,一直处于忙等状态,类似于一直在进行一个whilte循环。这样增加了CPU的开销,所以对于需要保持上下文的任务必须要使用自旋锁,同时它是不能被抢占的(高优先级会抢占低优先级的进程),适用于CPU密集型的任务。
递归锁
对于递归调的时候我们不能简单的使用某个锁来锁住临界区,这样就会造成死锁,必须要使用递归锁。当加锁的时候,变量就会加1,解锁的时候开始减1,直到值为0的时候释放锁。
读写锁
在上面提到的几种锁同一时刻只允许一个线程访问资源,在操作资源的时候,往往会出现一个线程在写操作,一个线程在读操作,这时候我们就会用到读写锁。 读写锁,它分为两种锁,读锁和写锁。当一个线程在读资源的时候,我们为其加上读锁,有另一个线程也要访问资源的时候我们需要再加一个读锁。当第三个线程需要进行写操作的时候我们就要加上一个写锁,但是这个写锁只有当读锁的使用者为0的时候才有效。也就是说我们必须等另外两个线程的读操作结束之后才能为其加上写锁。虽然互斥锁和自旋锁也能达到要求,但是读写锁无疑让控制更加精细,节省了CPU。
iOS中的锁
大神ibireme的一篇博客不再安全的 OSSpinLock对锁的性能做了简单的比较,大家可以看下。
OSSpinLock
自旋锁在此之前OSSpinLock是无疑性能最好的锁,在2015年的时候苹果工程师透露出了自旋锁的bug。具体问题大概如下: 系统维护了不同等级优先级的线程,高优先级的线程更容易获得系统资源。当一个低优先级的线程先获得锁之后,高优先级线程这时候也试图获取锁。这时候高优先级占据了大量的CPU,一直处于忙等状态。而低优先级的线程因为没有分配到足够的CPU一直无法完成任务,也就无法释放锁。这时候线程的调度就产生了优先级翻转。
Example
//定义OSSpinLock锁
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
OSSpinLockUnlock(&theLock);
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
sleep(1);
NSLog(@"需要线程同步的操作2");
OSSpinLockUnlock(&theLock);
});
sychronized
互斥锁 递归锁
iOS中性能最差的锁,但是写起来很开心,用起来简单,无论普通的锁还是递归锁都能应对。它牺牲了性能,保证了接口得友好。它会把每个传入的对象当作锁(这要求我们要保证对象的唯一性)并把他们放在一个哈希表中存储,通过这张表来管理锁。要注意的是,对象不能为nil,这会让临界区不再是线程安全的。
Example//需要加锁的对象
NSObject *obj = [[NSObject alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
}
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要线程同步的操作2");
}
});
Result
ThreadTest[13438:385647] 需要线程同步的操作1 开始
ThreadTest[13438:385647] 需要线程同步的操作1 结束
ThreadTest[13438:385646] 需要线程同步的操作2
NSLock
互斥锁将C语言的pthread_mutex锁进行了一次封装,更加的OC。NSLock
提供了tryLock
和lockBeforeDate:
方法。在加锁前尝试加锁,如果返回无法获得锁,并不会阻塞线程会继续往下走,返回NO
,lockBeforeDate:
让你控制加锁的时间,在规定时间未获得锁返回NO
Example
//创建锁
NSLock *lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//[lock lock];
//立即加锁
[lock lockBeforeDate:[NSDate date]];
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
//尝试加锁
if ([lock tryLock]) {
//尝试获取锁,如果获取不到返回NO,不会阻塞该线程
NSLog(@"锁可用的操作");
[lock unlock];
}else{
NSLog(@"锁不可用的操作");
}
NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
if ([lock lockBeforeDate:date]) {
//尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程
NSLog(@"没有超时,获得锁");
[lock unlock];
}else{
NSLog(@"超时,没有获得锁");
}
});
Result
ThreadTest[13541:394814] 需要线程同步的操作 1 开始
ThreadTest[13541:394804] 锁不可用的操作
ThreadTest[13541:394814] 需要线程同步的操作 1 结束
ThreadTest[13541:394804] 没有超时,获得锁
NSRecursiveLock
互斥锁 递归锁
实现和NSLock
差不多,只不过在底层NSLock使用的C的pthread_mutex_lock
,而NSRecursiveLock
使用的是pthread_mutex_recursive
。
它也同样提供了- (BOOL)tryLock
和- (BOOL)lockBeforeDate:(NSDate *)limit
方法
Example
//NSLock *lock = [[NSLock alloc] init];
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
如果使用NSLock
就会直接死锁。
Result
ThreadTest[13593:400360] value = 5
ThreadTest[13593:400360] value = 4
ThreadTest[13593:400360] value = 4
ThreadTest[13593:400360] value = 2
ThreadTest[13593:400360] value = 1
NSCondition
互斥锁 条件变量
条件变量和信号量很类似,提供了阻塞、等待就绪、唤醒线程的方法。比如,生产-消费者模式。 在单线程执行中,我们经常会通过一个布尔值来控制某个方法或者值是否需要被执行和修改。在多线程的环境下,需要互斥锁来配合保证线程安全。
Example
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSMutableArray *products = [NSMutableArray array];
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[lock lockWhenCondition:NO_DATA];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,总量:%zi",products.count);
[lock unlockWithCondition:HAS_DATA];
sleep(1);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"wait for product");
[lock lockWhenCondition:HAS_DATA];
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[lock unlockWithCondition:NO_DATA];
}
});
Result
ThreadTest[13645:406942] wait for product
ThreadTest[13645:406951] produce a product, 总量 :1
ThreadTest[13645:406942] custome a product
ThreadTest[13645:406942] wait for product
ThreadTest[13645:406951] produce a product, 总量:1
ThreadTest[13645:406942] custome a product
只有当unlock
和lock
的值一一对应才能唤醒线程,执行后续操作。
dispatch_semaphore(信号)
通过信号来控制同时执行的线程个数,来达到枷锁的目的。只有当信号大于0的时候才能执行接下去的操作。
//创建一个信号,同一时间只有一个线程可以执行,10即10个线程可以同时执行
dispatch_semaphore_create(1)
//等待信号>0的时候再执行下面的操作,并将信号减一
dispatch_semaphore_wait(self._lock, DISPATCH_TIME_FOREVER)
//信号+1
dispatch_semaphore_signal(self._lock)
Example
self._lock = dispatch_semaphore_create(1);
self.array = [[NSMutableArray alloc] initWithObjects:@(1),@(2),@(3), nil];
for (int i = 0; i < 100; i ++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(self._lock, DISPATCH_TIME_FOREVER);
NSLog(@"-----%d",i);
sleep(2);
dispatch_semaphore_signal(self._lock);
});
}
Result
MultiThread[10894:436938] -----0
MultiThread[10894:436939] -----1
MultiThread[10894:436945] -----2
MultiThread[10894:436937] -----3
MultiThread[10894:436936] -----4
以上除了第一个结果其他的每隔2s才打印出来
参考资料
Difference between binary semaphore and mutex