iOS开发中,多线程开发是个头疼的问题,最大的问题就是资源竞争问题。同一时间,多个线程对资源的读或者写都有可能造成不可预知的问题。解决这种问题的手段就是在操作资源的时候加上锁,那么常用的锁都有哪几种呢?本篇博客就来简单的说一说。
多线程卖票 - (void )sellTicket {
self .number = 5 ;
void (^sellTicket)() = ^() {
[self .lock lock];
if (self .number > 0 ) {
[NSThread sleepForTimeInterval:0.1 ];
self .number--;
NSLog (@"%@还有%ld张票" ,[NSThread currentThread],self .number);
}
[self .lock unlock];
};
for (int i = 0 ; i < 10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), sellTicket);
}
}
上面的代码是典型的多线程问题,现在有5张票,开启了10个窗口(三条线程),进行卖票(资源竞争),每个窗口只卖一张,卖票的时候判断了票的个数,但是输出结果依然有问题。输出如下:
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164897 ] <NSThread : 0x61000007bf80 >{number = 22 , name = (null)}还有1 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164888 ] <NSThread : 0x61000007ac80 >{number = 16 , name = (null)}还有0 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164893 ] <NSThread : 0x60000007c980 >{number = 17 , name = (null)}还有2 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164894 ] <NSThread : 0x600000076b00 >{number = 19 , name = (null)}还有2 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164895 ] <NSThread : 0x608000262b80 >{number = 20 , name = (null)}还有2 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164896 ] <NSThread : 0x600000077d40 >{number = 21 , name = (null)}还有2 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164427 ] <NSThread : 0x608000262c40 >{number = 15 , name = (null)}还有-1 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164892 ] <NSThread : 0x608000262a40 >{number = 18 , name = (null)}还有2 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164891 ] <NSThread : 0x61000007c280 >{number = 14 , name = (null)}还有-2 张票
2017 -06 -26 16 :03 :23.902 OCDemo[59833 :17164889 ] <NSThread : 0x60800007f600 >{number = 13 , name = (null)}还有-3 张票
这就是典型的多线程问题。在同一时间内多个线程共同操作一个数据,就会发生这种问题。那么解决这种问题最常用的手段就是加锁,当然,还有其他的解决办法,例如我们可以将操作数据的代码放到串行队列中,但这篇文章主要来说一说各种锁的用法。
锁的概念 锁是最常用的同步工具,一段代码在同一时间只能允许被一个线程访问,比如线程A进入加锁代码以后,线程会持有这个锁,当其他线程进入这段代码以后就无法访问,只有线程A释放掉锁以后其他线程才能继续访问这段代码。但是,通常情况下,我们不需要将整段代码放入到锁中,只需要将操作数据的部分代码加锁即可。
NSLock NSLock
是一个简单的互斥锁,实现了NSLocking
协议,主要方法如下:
- (void )lock;
- (void )unlock;
- (BOOL )tryLock;
- (BOOL )lockBeforeDate:(NSDate *)limit;
具体用法如下:
@property (nonatomic , strong ) NSLock *lock;
- (void )sellTicket {
self .number = 5 ;
void (^sellTicket)() = ^() {
[self .lock lock];
if (self .number > 0 ) {
[NSThread sleepForTimeInterval:0.1 ];
self .number--;
NSLog (@"%@还有%ld张票" ,[NSThread currentThread],self .number);
}
[self .lock unlock];
};
for (int i = 0 ; i < 10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), sellTicket);
}
}
@synchronized 利用关键字synchronized
将代码块同步,使用如下:
- (void )sellTicket {
self .number = 5 ;
void (^sellTicket)() = ^() {
@synchronized (self ) {
if (self .number > 0 ) {
[NSThread sleepForTimeInterval:0.1 ];
self .number--;
NSLog (@"%@还有%ld张票" ,[NSThread currentThread],self .number);
}
}
};
for (int i = 0 ; i < 10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), sellTicket);
}
}
@synchronized
使用self
作为锁,当然你也可以使用其他的对象。
NSCondition NSCondition
是一个条件锁,同样实现了NSLocking
协议,它和NSLock
一样,也有lock
和unlock
方法。它的基本用法和NSLock
一样,这里说一下NSCondition
的特殊用法。
NSCondition
提供更高级的用法,方法如下:
- (void )wait;
- (BOOL )waitUntilDate:(NSDate *)limit;
- (void )signal;
- (void )broadcast;
我们修改上面的买票例子,依然是开启10个窗口,每个窗口只卖一张票,如果票卖完了,就阻塞,一直等待,直到有票。
- (void )sellTicket {
self .number = 5 ;
void (^sellTicket)() = ^() {
[self .lock lock];
[self loopTicket];
[self .lock unlock];
};
for (int i = 0 ; i < 10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), sellTicket);
}
}
- (void )loopTicket {
if (self .number > 0 ) {
self .number--;
NSLog (@"%@还有%ld张票" ,[NSThread currentThread],self .number);
}else {
[self .lock wait];
if (self .number > 0 ) {
[self loopTicket];
}else {
NSLog (@"我不想等了" );
}
}
}
- (void )addTicket {
self .number = 2 ;
[self .lock broadcast];
}
调用addTicket
方法增加票数 并唤醒所有的阻塞线程。
NSConditionLock NSConditionLock
也实现了NSLocking
协议,但是它内部维护了一个条件,只有当锁可用并且条件满足时才会持有锁,所以NSConditionLock
既是条件锁也是互斥锁, 来看NSConditionLock
的方法:
- (instancetype )initWithCondition:(NSInteger )condition;
@property (readonly ) NSInteger condition;
- (void )lockWhenCondition:(NSInteger )condition;
- (BOOL )tryLock;
- (BOOL )tryLockWhenCondition:(NSInteger )condition;
- (void )unlockWithCondition:(NSInteger )condition;
- (BOOL )lockBeforeDate:(NSDate *)limit;
- (BOOL )lockWhenCondition:(NSInteger )condition beforeDate:(NSDate *)limit;
我们现在来做这样一个例子,假如有10个卖票窗口,每个窗口只卖一张票。我们有十个加票员,只有票卖完了,才去加票,每次只加一张票。如下:
- (void )addTicket {
[self .lock lockWhenCondition:0 ];
self .number = 1 ;
NSLog (@"我加了一张票,还有%ld张票" ,self .number);
[self .lock unlockWithCondition:1 ];
}
- (void )sellTicket {
self .number = 1 ;
void (^sellTicket)() = ^() {
[self .lock lockWhenCondition:1 ];
[self loopTicket];
[self .lock unlockWithCondition:0 ];
};
for (int i = 0 ; i < 10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), sellTicket);
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
[self addTicket];
});
}
}
- (void )loopTicket {
if (self .number > 0 ) {
self .number--;
NSLog (@"我卖了一张票%@还有%ld张票" ,[NSThread currentThread],self .number);
}
}
输出结果如下:
2017 -06 -27 11 :07 :51.454 OCDemo[71778 :17837792 ] 我卖了一张票<NSThread : 0x6000000798c0 >{number = 3 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.454 OCDemo[71778 :17838431 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.455 OCDemo[71778 :17838433 ] 我卖了一张票<NSThread : 0x608000078bc0 >{number = 4 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.455 OCDemo[71778 :17838436 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.455 OCDemo[71778 :17838437 ] 我卖了一张票<NSThread : 0x60000007e340 >{number = 5 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.455 OCDemo[71778 :17838434 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.456 OCDemo[71778 :17838441 ] 我卖了一张票<NSThread : 0x60000007e400 >{number = 6 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.456 OCDemo[71778 :17838431 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.456 OCDemo[71778 :17838442 ] 我卖了一张票<NSThread : 0x61800006f900 >{number = 7 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.457 OCDemo[71778 :17838438 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.457 OCDemo[71778 :17838444 ] 我卖了一张票<NSThread : 0x60000007e440 >{number = 8 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.457 OCDemo[71778 :17838436 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.457 OCDemo[71778 :17838445 ] 我卖了一张票<NSThread : 0x618000072280 >{number = 9 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.457 OCDemo[71778 :17838433 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.458 OCDemo[71778 :17838435 ] 我卖了一张票<NSThread : 0x610000075040 >{number = 10 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.458 OCDemo[71778 :17838434 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.458 OCDemo[71778 :17838446 ] 我卖了一张票<NSThread : 0x61800006f800 >{number = 11 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.458 OCDemo[71778 :17837792 ] 我加了一张票,还有1 张票
2017 -06 -27 11 :07 :51.459 OCDemo[71778 :17838440 ] 我卖了一张票<NSThread : 0x608000078c80 >{number = 12 , name = (null)}还有0 张票
2017 -06 -27 11 :07 :51.459 OCDemo[71778 :17838437 ] 我加了一张票,还有1 张票
NSRecursiveLock NSRecursiveLock
是一个递归锁,有时候加锁代码
中存在递归调用,递归开始前加锁,递归调用开始后,反复执行加锁代码会造成死锁,这个时候可以使用递归锁来解决问题。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程会记录获取锁和释放锁的次数,注意,只有两者平衡锁才会被最终释放。
如下,我们先模拟一下递归死锁,我们使用NSLock
锁:
self .lock = [NSLock new];
- (void )loopLock {
[self .lock lock];
if (self .number > 0 ) {
self .number--;
NSLog (@"还有%ld张票" ,self .number);
[self loopLock];
}
[self .lock unlock];
}
这里直接模拟主线程卖票,卖了一张以后 还想卖,结果造成死锁。如果我们使用NSRecursiveLock
,self.lock = [NSRecursiveLock new]
将不会造成死锁。一定要注意,加锁和解锁是成对出现的。
dispatch_semaphore_t 条件信号量,dispatch_semaphore_t
是GCD中的信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1,每当发送一个等待信号时,信号量-1.如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。 开如下方法:
dispatch_semaphore_create(long value); 创建一个信号 初始化信号量 这里你可以随意设置
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信号 当信号的信号量为0 的时候会阻塞线程 直到信号的信号量大于0 超时时间为timeout 当信号的信号量大于0 会将信号量减1 。
dispatch_semaphore_signal(dispatch_semaphore_t dsema); 发送一个信号量 此时信号的信号量会+1
示例如下:
@property (nonatomic , strong ) dispatch_semaphore_t semaphore;
- (void )viewDidLoad {
[super viewDidLoad];
self .number = 10 ;
self .semaphore = dispatch_semaphore_create(1 );
}
- (void )test {
for (int i = 0 ; i<10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
[self sellTicket];
});
}
}
- (void )sellTicket {
dispatch_semaphore_wait(self .semaphore, DISPATCH_TIME_FOREVER);
if (self .number > 0 ) {
self .number--;
NSLog (@"%@还剩%ld张票" ,[NSThread currentThread],self .number);
}
dispatch_semaphore_signal(self .semaphore);
}
POSIX POSIX
是互斥所,和dispatch_semaphore_t
很像,但是完全不同,POSIX
是Unix/Linux
平台上提供的一套条件互斥锁的API。
新建一个简单的POSIX
互斥锁pthread_mutex_t
,引入头文件#import <pthread.h>
声明并初始化一个pthread_mutex_t
的结构。使用pthread_mutex_lock
和pthread_mutex_unlock
函数。调用pthread_mutex_destroy
来释放该锁的数据结构。
如下:
@interface ViewController ()
{
pthread_mutex_t mutex;
}
@end
@implementation ViewController
- (void )viewDidLoad {
[super viewDidLoad];
pthread_mutex_init(&mutex, NULL );
}
- (void )test {
for (int i = 0 ; i<10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
[self sellTicket];
});
}
}
- (void )sellTicket {
pthread_mutex_lock(&mutex);
if (self .number > 0 ) {
self .number--;
NSLog (@"%@还剩%ld张票" ,[NSThread currentThread],self .number);
}
pthread_mutex_unlock(&mutex);
}
- (void )dealloc {
pthread_mutex_destroy(&mutex);
}
POSIX
还可以创建条件锁pthread_cond_t
,提供了和NSCondition
一样的条件控制,初始化互斥锁同时使用pthread_cond_init
来初始化条件数据结构:
int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast (pthread_cond_t *cond);
int pthread_cond_destroy (pthread_cond_t *cond);
POSIX
还提供了很多函数,有一套完整的API,包含Pthreads
线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。推荐一篇详细文章,但不是基于iOS
的,是基于Linux
的,但是介绍的非常详细Linux 线程锁详解
OSSpinLock OSSpinLock
是自旋锁,首先要声明的一点,OSSpinLock
不再安全。主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。具体为什么不安全,请看这篇文章不再安全的 OSSpinLock ;
玩法如下:
#import <libkern/OSAtomic.h>
@interface ViewController ()
{
OSSpinLock spinlock;
}
@end
@implementation ViewController
- (void )viewDidLoad {
[super viewDidLoad];
self .number = 10 ;
spinlock = OS_SPINLOCK_INIT;
}
- (IBAction )test:(id )sender {
for (int i = 0 ; i<10 ; i++) {
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
[self sellTicket];
});
}
}
- (void )sellTicket {
OSSpinLockLock(&spinlock);
if (self .number > 0 ) {
self .number--;
NSLog (@"%@还剩%ld张票" ,[NSThread currentThread],self .number);
}
OSSpinLockUnlock(&spinlock);
}
@end
最后放一张各个锁的性能对比图(摘自ibireme ):
从图中可以看出,OSSpinLock
的性能最好,但是它已不再安全。推荐大家使用dispatch_semaphore
。