想要深入理解多线程,锁是预备知识,这里总结一下OC中锁相关的知识,打好基础。
为什么要有锁?
锁概念的提出,是为了解决多线程资源共享的问题,在多线程环境下,有的资源可能会同时被多个线程访问,可能会出现资源抢夺的问题。这里引入一个概念叫临界区(Critical Section),就是一段代码,同一时间只能由一个线程访问,以保障临界区内的线程是安全的(资源不被抢夺,改变)。锁就是用来解决临界区内线程安全的问题。
下面介绍三种常见的锁:
自旋锁(spin lock)
自旋锁长这样:
while (抢锁(lock) == 没抢到) {
}
就是利用一个while
循环,不断的尝试去抢锁(这里的锁lock
是一个抽象的概念,可以是一个整数,一开始是1,表示没有锁,抢到后变为0,表示有锁),抢到了锁就跳出循环,抢不到锁就不断重试。
自旋锁的缺点很明显,不断的抢锁会占用CPU资源。优点是线程不用休眠,不用花时间在上下文切换(context switch)从用户态转化为内核态,用在轻量级的临界区上效率高。
互斥锁(mutex)
互斥锁长这样:
while (抢锁(lock) == 没抢到) {
线程休眠,请在这把锁的状态发生改变时再唤醒(lock);
}
和自旋锁很相似,不同就是抢不到锁的时候,让线程去休眠,当锁的状态改变的时候再唤醒该线程。
互斥锁的缺点是,线程休眠会让线程从用户态转化为内核态,唤醒的时候从内核态转化为用户态,需要两次上下文切换,花费大量时间。优点是不用忙等,临界区很长时效率高。
读写锁(readers-writer lock)
读写锁就是分了两种情况,一种是读时的锁,一种是写时的锁,同时规定:
- 同时可以存在多个读锁,也就是读-读不互斥
- 只能存在一个写锁,也就是读-写互斥,写-写互斥
读写锁的实现时用了两个互斥锁(或者两个自旋锁):
//读者加锁
- (void)readerLock {
加锁(rlock);
condition++;
if (condition == 1) {
加锁(wlock);
}
解锁(rlock);
}
//读者解锁
- (void)readerUnlock {
加锁(rlock);
condition--;
if (condition == 0) {
加锁(wlock);
}
解锁(rlock);
}
//写者加锁
- (void)writerLock {
加锁(wlock);
}
//写者解锁
- (void)writerUnlock {
解锁(wlock);
}
@end
这里我们用了两把互斥锁(rlock
,wlock
)来实现读写锁,利用了:
- 计数器
condition
跟踪被阻塞的读线程。如果先有写锁,读锁中condition==1
,读会被wlock
阻塞。如果先有读锁,写锁会被wlock
堵塞;读锁再次获取时,可以使condition>1
,从而读锁不被堵塞。 - 互斥锁
rlock
保护condition
,供读者使用 - 互斥锁
wlock
确保写操作互斥
下面介绍一个更高级的实现读写锁的方法:条件变量+互斥锁
首先介绍一下条件变量:
条件变量可以简单理解为,一个条件,如果达成了就发通知。这样说有点抽象,把条件变量用到读写锁里就清楚了:
//读者加锁
- (void)readerLock {
加锁(rwlock);
while (self.isWriting) {
解锁,等待条件变量达成时的通知唤醒,再加锁(cond, rwlock);
}
self.readCount++;
解锁(rwlock);
}
//读者解锁
- (void)readerUnlock {
加锁(rwlock);
self.readCount--;
if (self.readCount == 0) {
//唤起一条写的线程
条件变量达成时,触发通知(cond);
}
解锁(rwlock);
}
//写者加锁
- (void)writerLock {
加锁(rwlock);
while (self.isWriting || self.readCount > 0) {
解锁,等待条件变量达成时的通知唤醒,再加锁(cond, rwlock);
}
self.isWriting = YES;
解锁(rwlock);
}
//写者解锁
- (void)writerUnlock {
加锁(rwlock);
self.isWriting = NO;
//唤起多个读的线程
条件变量达成时的,触发通知(cond);
解锁(rwlock);
}
@end
这里使用了使用[条件变量cond
与普通的互斥锁rwlock
、整型计数器readCount
(表示正在读的个数)与布尔标志isWrite
(表示正在写)来实现读写锁。
- 当有读锁时,
readCount>0
,写锁进入while
循环,只有条件变量达成时,才会收到通知唤醒跳出循环,条件变量达成的条件就是读锁全部释放(readCount==0
)。 - 当有写锁时,
isWriting == YES
,读锁进入while
循环,只有条件变量达成时,才会收到通知唤醒出循环,条件变量达成的条件就是写锁释放(isWriting == NO
)。
下面总结一个OC钟常用锁的用法,一下所有例子,都用卖股票的例子:
@synchronized 关键字
@synchronized(这里添加一个OC对象,一般使用self) {
要加锁的代码
}
这是一个互斥锁,简单易用,但性能最差,建议加锁的代码尽量少,例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
//设置票的数量为5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
//线程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//线程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
@synchronized(self) {
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@", [NSThread currentThread]);
break;
}
}
}
}
// 剩余票数=4, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 票卖完了 Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 票卖完了 Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
NSLock
_mutexLock = [[NSLock alloc] init];
[_mutexLock lock];
[_mutexLock unlock];
互斥锁,有lock
和unlock
方法:
- (void)viewDidLoad {
[super viewDidLoad];
//设置票的数量为5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
_mutexLock = [[NSLock alloc] init];
//线程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//线程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
[_mutexLock lock];
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@", [NSThread currentThread]);
break;
}
[_mutexLock unlock];
}
}
// 剩余票数=4, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 票卖完了 Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
pthread_mutex
pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);
互斥锁
- (void)viewDidLoad {
[super viewDidLoad];
//设置票的数量为5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
// @property pthread_mutex_t mutex;
pthread_mutex_init(&_mutex, NULL);
//线程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//线程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
pthread_mutex_lock(&_mutex);
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@", [NSThread currentThread]);
break;
}
pthread_mutex_unlock(&_mutex);
}
}
@end
// 剩余票数=4, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 票卖完了 Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
dispatch_semaphore
_semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(_semaphore);
信号量实现加锁,线程获取一个信号量时,信号量数量减一,线程释放信号量时,信号量数量加一,信号量数量大于等于零时,加锁的代码可以执行。互斥锁可以看做是一种特殊的信号量(初始信号量等于一)。
- (void)viewDidLoad {
[super viewDidLoad];
//设置票的数量为5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
_semaphore = dispatch_semaphore_create(1);
//线程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//线程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@", [NSThread currentThread]);
break;
}
dispatch_semaphore_signal(_semaphore);
}
}
@end
// 剩余票数=4, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 票卖完了 Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
OSSpinLock
_pinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&_pinLock);
OSSpinLockUnlock(&_pinLock);
自旋锁,效率最高,但有隐患:
可能会出现优先级翻转的情况。比如线程1优先级比较高,线程2优先级比较低,然后在某一时刻是线程2先获取到锁,所以先是线程2加锁,这时候,线程1就在while(目标锁还未释放),这个状态,但因为线程1优先级比较高,所以系统分配的时间比较多,有可能会没有分配时间给线程2执行后续的操作(需要做的任务和解锁)了,这时候就会造成死锁。
所以iOS10之后自旋锁OSSpinLock
就被os_unfair_lock
(底层是互斥锁)替换了。
- (void)viewDidLoad {
[super viewDidLoad];
//设置票的数量为5
self.tickets = 5;
self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
// #import <libkern/OSAtomic.h>
// @property OSSpinLock pinLock;
// 'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
_pinLock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(self.q1, ^{
[self saleTickets];
});
//线程2
dispatch_async(self.q1, ^{
[self saleTickets];
});
}
- (void)saleTickets {
while (1) {
[NSThread sleepForTimeInterval:1];
OSSpinLockLock(&_pinLock);
if (self.tickets > 0) {
self.tickets -= 1;
NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"票卖完了 Thread:%@", [NSThread currentThread]);
break;
}
OSSpinLockUnlock(&_pinLock);
}
}
@end
// 剩余票数=4, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 票卖完了 Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}