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,查看。
参考资料