iOS开发中遇到的同步机制 发表于 2017-05-02 | 更新于 2022-07-26
| 阅读量:
今天主要是来讨论下,线程同步机制的问题。说到线程同步,一般人可能会想到“NSLock” 、“@synchronized” 、“GCD信号量” 等等,好吧,其实这是我想到的,然而我要说的是,如果在面试中只答到这么几个可是远远不够的。所以我查找了下资料,这才发现原来ios中线程同步的方法可足足有将近10种,且听我娓娓道来。
各个锁进行1000000此的加锁解锁的空操作时间如下
1 2 3 4 5 6 7 8 9 OSSpinLock: 46.15 ms dispatch_semaphore: 56.50 ms pthread_mutex: 178.28 ms NSCondition : 193.38 msNSLock : 175.02 mspthread_mutex(recursive): 172.56 ms NSRecursiveLock : 157.44 msNSConditionLock : 490.04 ms@synchronized : 371.17 ms
NSLock 提到NSLock,首先要提另外一个名词叫NSLocking,这是一个协议,主要就定义了俩个方法,一个叫 lock,一个叫unLock。NSLock其实就是确认了NSLocking协议的一个NSObject对象,那么NSLock如何使用呢?其实很简单,就是在你认为可能会发生多线程访问的地方进行lock 操作,在执行完相应代码之后,执行unlock操作。举个简单例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 -(void )testLock{ __block NSString *name = @"成焱" ; NSOperationQueue *queue = [[NSOperationQueue alloc]init]; queue.maxConcurrentOperationCount = 3 ; NSLock *lock = [NSLock new]; [queue addOperationWithBlock:^{ NSLog (@"1 将要上锁" ); [lock lock]; NSLog (@"1 已上锁,访问资源" ); name = @"哇哈哈" ; sleep(3 ); NSLog (@"1 将要解锁" ); [lock unlock]; NSLog (@"1 已解锁" ); }]; [queue addOperationWithBlock:^{ sleep(1 ); NSLog (@"2 将要上锁" ); [lock lock]; NSLog (@"2 已上锁,访问资源" ); name = @"康师傅" ; sleep(2 ); NSLog (@"2 将要解锁" ); [lock unlock]; NSLog (@"2 已解锁" ); }]; }
打印结果如下
1 2 3 4 5 6 7 8 2017 -04 -24 23 :06 :59.831694 test[1300 :102434 ] 1 将要上锁2017 -04 -24 23 :06 :59.831718 test[1300 :102434 ] 1 已上锁,访问资源2017 -04 -24 23 :07 :00.835992 test[1300 :102435 ] 2 将要上锁2017 -04 -24 23 :07 :02.836242 test[1300 :102434 ] 1 将要解锁2017 -04 -24 23 :07 :02.836385 test[1300 :102434 ] 1 已解锁2017 -04 -24 23 :07 :02.836443 test[1300 :102435 ] 2 已上锁,访问资源2017 -04 -24 23 :07 :04.841407 test[1300 :102435 ] 2 将要解锁2017 -04 -24 23 :07 :04.841528 test[1300 :102435 ] 2 已解锁
查看控制台的打印输出很明显的看到了,在线程一访问name时,加锁之后,线程2一直在等待,直到线程1释放锁之后,线程2才会去访问name。
@synchronize 想必但凡是开发过一段时间ios程序的同学,一定会对这个关键字不陌生。这个关键字的字面意思就是“同步”。那么它是如何实现同步的呢?
该特性允许传入一个NSObject类型的对象,并执行一个block,形如
网上查询资料获得,这个特性其实是对objc_sync_enter()于objc_sync_exit()的封装,其实际上等价于
1 2 3 4 5 @try { objc_sync_enter(obj); }@finally { objc_sync_exit(obj); }
函数objc_sync_enter()内部实际进行的操作,是对传入的对象,分配递归锁,并存在哈希表中,感兴趣的同学可以参考这篇blog ,在这里我就不展开讨论了。不过下面还是举个简单例子来说明下如何使用这个特性,这里有个地方需要注意就是,**当传入的对象为nil时,将会从代码中移走线程安全**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - (void )testSynchronized { __block NSString *source = @"资源" ; dispatch_queue_t global = dispatch_get_global_queue(0 , 0 ); dispatch_async (global, ^{ @synchronized (source) { NSLog (@"1 将要执行" ); sleep(3 ); NSLog (@"1 执行完毕" ); } }); dispatch_async (global, ^{ sleep(1 ); @synchronized (source) { NSLog (@"2 将要执行" ); sleep(1 ); NSLog (@"2 执行完毕" ); } }); }
打印结果如下
1 2 3 4 2017 -04 -24 23 :43 :01.933835 test[1589 :154385 ] 1 将要执行2017 -04 -24 23 :43 :04.938447 test[1589 :154385 ] 1 执行完毕2017 -04 -24 23 :43 :04.938782 test[1589 :154386 ] 2 将要执行2017 -04 -24 23 :43 :05.942177 test[1589 :154386 ] 2 执行完毕
信号量 GCD的信号量机制,通过消耗信号的方式,控制线程同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 - (void )testSemaphore { dispatch_queue_t global = dispatch_get_global_queue(0 , 0 ); dispatch_semaphore_t semaphore = dispatch_semaphore_create(1 ); dispatch_async (global, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog (@"1 将要执行" ); sleep(3 ); NSLog (@"1 执行完毕" ); dispatch_semaphore_signal(semaphore); }); dispatch_async (global, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog (@"2 将要执行" ); sleep(3 ); NSLog (@"2 执行完毕" ); dispatch_semaphore_signal(semaphore); }); }
***打印结果如下***
1 2 3 4 2017 -04 -26 22 :32 :17.563019 test[914 :142382 ] 1 将要执行2017 -04 -26 22 :32 :20.568142 test[914 :142382 ] 1 执行完毕2017 -04 -26 22 :32 :20.568336 test[914 :142383 ] 2 将要执行2017 -04 -26 22 :32 :23.573598 test[914 :142383 ] 2 执行完毕
NSConditionLock 条件锁,当满足某种条件时,才会尝试获取锁,利用该特性,可以人为干预线程执行的依赖顺序,参见如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 - (void )testConditionLock { int condition = 1 ; NSConditionLock *conditionLock = [[NSConditionLock alloc]initWithCondition:condition]; dispatch_queue_t global = dispatch_get_global_queue(0 , 0 ); dispatch_async (global, ^{ BOOL islocked = [conditionLock tryLockWhenCondition:1 ]; NSLog (@"线程1 要执行" ); sleep(2 ); NSLog (@"线程1 执行完毕" ); if (islocked) { [conditionLock unlockWithCondition:3 ]; } }); dispatch_async (global, ^{ BOOL isLocked = [conditionLock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:10 ]]; NSLog (@"线程2 要执行" ); sleep(2 ); NSLog (@"线程2 执行完毕" ); if (isLocked) { [conditionLock unlockWithCondition:1 ]; } }); dispatch_async (global, ^{ BOOL isLocked = [conditionLock tryLockWhenCondition:3 ]; NSLog (@"线程3 要执行" ); sleep(3 ); NSLog (@"线程3 执行完毕" ); if (isLocked) { NSLog (@"加锁了" ); [conditionLock unlockWithCondition:10 ]; } }); }
打印结果如下
1 2 3 4 5 6 2017 -04 -26 23 :11 :56.343532 test[1092 :199052 ] 线程1 要执行2017 -04 -26 23 :11 :56.343546 test[1092 :199054 ] 线程3 要执行2017 -04 -26 23 :11 :58.348745 test[1092 :199052 ] 线程1 执行完毕2017 -04 -26 23 :11 :59.348618 test[1092 :199054 ] 线程3 执行完毕2017 -04 -26 23 :12 :06.348449 test[1092 :199053 ] 线程2 要执行2017 -04 -26 23 :12 :08.353647 test[1092 :199053 ] 线程2 执行完毕
dispatch_barrier_async()与dispatch_barrier_sync() GCD提供了线程顺序控制的一个函数,假设有5个任务要执行,需要前俩个并发执行,执行完成之后执行第三个任务,等第三个执行完成才可以执行第四个和第五个任务,这个时候就可以考虑使用dispath_barrier_async()函数,具体dispatch_barrier_asyn与dispatch_barrier_sync有什么区别的话,稍后再说,请看下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 - (void )testBarrierAsyncAndSync { dispatch_queue_t global = dispatch_queue_create("com.demo.chengyan" , DISPATCH_QUEUE_CONCURRENT); dispatch_async (global, ^{ NSLog (@"任务1" ); }); dispatch_async (global, ^{ NSLog (@"任务2" ); }); dispatch_barrier_async(global, ^{ sleep(3 ); NSLog (@"任务3" ); }); NSLog (@"---------------" ); dispatch_async (global, ^{ NSLog (@"任务4" ); }); dispatch_async (global, ^{ NSLog (@"任务5" ); }); }
打印结果如下
1 2 3 4 5 6 2017 -04 -26 23 :43 :08.653034 test[1214 :243421 ] ---------------2017 -04 -26 23 :43 :08.653138 test[1214 :243447 ] 任务1 2017 -04 -26 23 :43 :08.653151 test[1214 :243448 ] 任务2 2017 -04 -26 23 :43 :11.654589 test[1214 :243447 ] 任务3 2017 -04 -26 23 :43 :11.654715 test[1214 :243447 ] 任务4 2017 -04 -26 23 :43 :11.654728 test[1214 :243448 ] 任务5
可以看到由于是通过async的方式添加到队列中的,所以没有阻塞主线程,—————–被最先执行了。同时注意到在任务3中,沉睡了3秒,而任务4和5都在等任务3执行完之后,才开始执行的。
那么接下来再看下sync的方式执行barrier会怎么样?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 - (void )testBarrierAsyncAndSync { dispatch_queue_t global = dispatch_queue_create("com.demo.chengyan" , DISPATCH_QUEUE_CONCURRENT); dispatch_async (global, ^{ NSLog (@"任务1" ); }); dispatch_async (global, ^{ NSLog (@"任务2" ); }); dispatch_barrier_sync(global, ^{ sleep(3 ); NSLog (@"任务3" ); }); NSLog (@"---------------" ); dispatch_async (global, ^{ NSLog (@"任务4" ); }); dispatch_async (global, ^{ NSLog (@"任务5" ); }); }
打印结果如下
1 2 3 4 5 6 2017-04-26 23:53:57.275541 test[1249:258152] 任务1 2017-04-26 23:53:57.275542 test[1249:258154] 任务2 2017-04-26 23:54:00.280724 test[1249:258129] 任务3 2017-04-26 23:54:00.280832 test[1249:258129] --------------- 2017-04-26 23:54:00.280944 test[1249:258154] 任务4 2017-04-26 23:54:00.281000 test[1249:258152] 任务5
可以看到由于sync的方式,阻塞了主线程的操作,导致主线程后面的打印必须要等任务3完成之后才会执行。所以尽量不要用这种方式在主线程调用。防止卡到ui
GCD的串行队列实际上也是可以起到锁的作用(略) os_unfair_lock(系统非公平锁) 在IOS10,MacOS10.12之后,苹果新提供的锁,用来替代OSSPinLock,根据官方文档说明,该锁解决了OSSPinLock的优先级反转问题,主要是通过该锁上携带的值以及它持有线程的所有权信息,系统可以以此做出相应的策略,来解决优先级反转的问题。就像它的名字一样,这是个非公平锁。
使用此锁,需要注意的是
unlock和lock操作必须得在同一个线程中,如果在不同的线程中解锁,将会导致线程直接crash。
该锁决不能通过shared或者mutiplay_mapped memory的方式,在多线程或者多进程中访问。因为该锁的实现,依赖于该锁的值和所在的进程。
该锁主要是为了替代废弃的OSSPinLock,但是它在争夺资源的时候,不是靠自旋,而是在内核上等待唤醒。
Mac系统开发要在10.12 之后只用,iOS需要在iOS10 以后才能使用
最后要说明的是,此锁就像它的名字一样,是非公平的,具体啥意思呢?举个例子,解开锁的消费者,存在一种可能立即又重新获得了锁,导致那些在休眠中等待的不能被唤醒。这可能是处于性能的考虑,但是确实有一种可能导致等待者处于饥饿状态。
下面的代码简要演示下如何使用该锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #import <os/lock.h> - (void )testUnfairlock { os_unfair_lock_t unfairlock = &OS_UNFAIR_LOCK_INIT; dispatch_queue_t queue = dispatch_get_global_queue(0 , 0 ); dispatch_async (queue, ^{ sleep(1 ); os_unfair_lock_lock(unfairlock); NSLog (@"线程1 将要执行" ); sleep(3 ); NSLog (@"线程1 执行结束" ); os_unfair_lock_unlock(unfairlock); }); dispatch_async (dispatch_get_main_queue(), ^{ os_unfair_lock_lock(unfairlock); NSLog (@"线程2 将要执行" ); sleep(2 ); NSLog (@"线程2 执行结束" ); os_unfair_lock_unlock(unfairlock); }); }
打印结果如下
1 2 3 4 2017 -04 -28 00 :14 :11.692782 test[892 :121163 ] 线程2 将要执行2017 -04 -28 00 :14 :13.698007 test[892 :121163 ] 线程2 执行结束2017 -04 -28 00 :14 :13.698230 test[892 :121192 ] 线程1 将要执行2017 -04 -28 00 :14 :16.700588 test[892 :121192 ] 线程1 执行结束
pthread_mutex_t(互斥锁) 互斥锁和自旋锁的区别,主要就在于,当获取锁失败之后,自旋锁会一直轮询,而互斥锁会轮询大概一秒之后,进入休眠,等待唤醒,此外互斥锁,是有队列概念的,有一个等待队列,依次唤醒等待者。
互斥锁和NSLock的区别则在于互斥锁trylock返回正确时返回0,错误时返回错误值,而NSLock则只会返回NO和YES。
此外互斥锁也是性能比较高的锁。使用方式的话,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 - (void )testPthreadMutex { static pthread_mutex_t plock; pthread_mutex_init(&plock, NULL ); dispatch_queue_t queue = dispatch_get_global_queue(0 , 0 ); dispatch_async (queue, ^{ sleep(1 ); pthread_mutex_lock(&plock); NSLog (@"线程1 将要执行" ); sleep(3 ); NSLog (@"线程1 执行结束" ); pthread_mutex_unlock(&plock); }); dispatch_async (dispatch_get_main_queue(), ^{ int code = pthread_mutex_lock(&plock); NSLog (@"线程2 将要执行,code = %d" ,code); sleep(2 ); NSLog (@"线程2 执行结束" ); pthread_mutex_unlock(&plock); }); }
执行结果如下
1 2 3 4 2017 -04 -28 00 :14 :11.692782 test[892 :121163 ] 线程2 将要执行2017 -04 -28 00 :14 :13.698007 test[892 :121163 ] 线程2 执行结束2017 -04 -28 00 :14 :13.698230 test[892 :121192 ] 线程1 将要执行2017 -04 -28 00 :14 :16.700588 test[892 :121192 ] 线程1 执行结束
NSRecursiveLock(递归锁) 此锁和NSLock的区别主要在于内部实现原理的不同,NSLock内部封装的pthread_mutex_t类型为PTHREAD_MUTEX_TIMED_NP,而NSRecursiveLock的类型为PTHREAD_MUTEX_RECURSIVE_NP,此外还有PTHREAD_MUTEX_ERRORCHECK_NP(检错锁)、PTHREAD_MUTEX_ADAPTIVE_NP(适应锁)。
递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
该锁的使用场景主要是在递归函数内部调用。
按照惯例还是看个代码来说明下如何使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 - (void )testRecursiveLock { NSRecursiveLock *recursive = [[NSRecursiveLock alloc]init]; dispatch_queue_t queue = dispatch_get_global_queue(0 , 0 ); dispatch_async (queue, ^{ [self getFactorial:10 cursive:recursive]; }); } - (int )getFactorial:(int )n cursive:(NSRecursiveLock *)lock { int result = 0 ; NSLog (@"加锁" ); [lock lock]; if (n <= 0 ) { result = 1 ; }else { result = [self getFactorial:n-1 cursive:lock] * n; } NSLog (@"result =%d" ,result); [lock unlock]; NSLog (@"解锁" ); return result; }
打印结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 2017 -05 -02 00 :43 :08.241134 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241902 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241920 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241934 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241942 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241950 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241957 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241974 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241991 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.241998 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.242005 test[1194 :551965 ] 加锁2017 -05 -02 00 :43 :08.242023 test[1194 :551965 ] result =1 2017 -05 -02 00 :43 :08.242030 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242036 test[1194 :551965 ] result =1 2017 -05 -02 00 :43 :08.242053 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242095 test[1194 :551965 ] result =2 2017 -05 -02 00 :43 :08.242110 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242128 test[1194 :551965 ] result =6 2017 -05 -02 00 :43 :08.242136 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242143 test[1194 :551965 ] result =24 2017 -05 -02 00 :43 :08.242151 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242293 test[1194 :551965 ] result =120 2017 -05 -02 00 :43 :08.242309 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242317 test[1194 :551965 ] result =720 2017 -05 -02 00 :43 :08.242324 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242331 test[1194 :551965 ] result =5040 2017 -05 -02 00 :43 :08.242338 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242344 test[1194 :551965 ] result =40320 2017 -05 -02 00 :43 :08.242351 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242357 test[1194 :551965 ] result =362880 2017 -05 -02 00 :43 :08.242365 test[1194 :551965 ] 解锁2017 -05 -02 00 :43 :08.242371 test[1194 :551965 ] result =3628800 2017 -05 -02 00 :43 :08.242378 test[1194 :551965 ] 解锁
NSCondition NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
使用方式主要包括,lock,unlock, wait, signal,四个方法,分别指获取锁、放开锁、等待信号、发送信号。同样用一段示例代码来看下它的用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 - (void )testCondition { NSCondition *condition = [NSCondition new]; NSMutableArray *ops = [NSMutableArray array]; dispatch_queue_t queue = dispatch_get_global_queue(0 , 0 ); dispatch_async (queue, ^{ [condition lock]; NSLog (@"1 将要上锁" ); while (ops.count == 0 ) { NSLog (@"1 等待" ); [condition wait]; } NSLog (@"1 移除第一个元素" ); [ops removeObjectAtIndex:0 ]; NSLog (@"1 将要解锁" ); [condition unlock]; }); dispatch_async (queue, ^{ NSLog (@"2 将要上锁" ); [condition lock]; NSLog (@"2 生产一个对象" ); [ops addObject:[NSObject new]]; NSLog (@"2 发送信号" ); [condition signal]; NSLog (@"2 将要解锁" ); [condition unlock]; }); }
打印结果如下
1 2 3 4 5 6 7 8 2017 -05 -02 01 :04 :28.327316 test[1240 :582713 ] 2 将要上锁2017 -05 -02 01 :04 :28.327329 test[1240 :582712 ] 1 将要上锁2017 -05 -02 01 :04 :28.328121 test[1240 :582712 ] 1 等待2017 -05 -02 01 :04 :28.328149 test[1240 :582713 ] 2 生产一个对象2017 -05 -02 01 :04 :28.328166 test[1240 :582713 ] 2 发送信号2017 -05 -02 01 :04 :28.328182 test[1240 :582713 ] 2 将要解锁2017 -05 -02 01 :04 :28.328233 test[1240 :582712 ] 1 移除第一个元素2017 -05 -02 01 :04 :28.328261 test[1240 :582712 ] 1 将要解锁
至此,ios里面的大部分同步方法我们已经基本了解了,剩下的就是在实践中选择合适的方法进行应用了。
附录 如果有想查看DEMO的同学,可以点击它 来下载DEMO,查看。
参考资料