当多线程访问共享资源的时候,会出现资源竞争的情况,导致数据错乱的问题,OC中要想实现线程间的同步的话,有以下手段:
一、锁
锁就是实现多线程同步的其中一种方案,查看示例程序,
总数是15
程序执行完成应该是0
,然而并不是,现在要做的就是让多线程对共享资源这里是
1 | int preCount = self.totalCount; |
能够顺序执行。
1.1、自旋锁
所谓的自旋锁,形如以下形式
1 | while( 抢锁 == 没抢到 ) { |
只要没有抢到锁,程序就会一直重试,所以会一直占用CPU资源。
1.1.1 OSSpinLockLock
跟踪下OSSpinLockLock
的汇编代码内部确实是一个while
循环,循环部分的代码如下
1 | 0x104ba98b1 <+12>: cmpl $-0x1, %eax |
OSSpinLockLock
特点:
- 效率高,因为一直占用着cpu,其实这个是优点也是缺点
- 会出现优先级反转(对于优先级不同的线程,使用同一把锁访问共同资源,优先级低的线程先拿到锁,这时优先级高的线程过来后,由于锁被占用,但是优先级高,一直占有CPU资源,导致持有锁的低优先级的线程无法执行释放锁的操作,从而优先级高的线程一直处于忙等的状态)的问题;
- 无法处理递归。
OSSpinLockLock使用见demo
1.2、互斥锁
由于自旋锁一直占用着cpu资源
1 | while( 抢锁 == 没抢到 ) { |
其实不需要一直循环等待,只要当检测到锁被占用了,那么就去睡觉,当锁的状态改变了,通知它就行了,这就是互斥锁。
1 | while (抢锁 == 没抢到) { |
1.2.1 os_unfair_lock
由于OSSpinLockLock
存在优先级反转的问题,os_unfair_lock
是苹果用来替代OSSpinLockLock
的。跟踪了汇编代码,等待的汇编代码如下
跟踪路径:os_unfair_lock_lock
-> _os_unfair_lock_lock_slow
-> __ulock_wait
1 |
|
一旦执行到syscall
时,该条线程就去睡觉了。
os_unfair_lock
特点:
- 没有优先级反转的问题;
- 由于线程睡觉,当然唤醒也是要时间的,效率没有自旋锁高;
iOS10.0
以上才可用;- 也不能处理递归的场景。
os_unfair_lock使用见demo
1.2.2 pthread_mutex_t
这个是一个比较底层的api,在C
和C++
都可以使用,这个对于没有抢到的资源也是睡觉,所以是互斥锁,通过断点可以查看休眠的代码
1 | libsystem_kernel.dylib`__psynch_mutexwait: |
找到该代码的路径为pthread_mutex_lock -> pthread_mutex_firstfit_lock_slow -> _pthread_mutex_firstfit_lock_wait -> __psynch_mutexwait
pthread_mutex_t
特点:
- 没有优先级反转的问题;
- 由于线程睡觉,当然唤醒也是要时间的,效率没有自旋锁高;
- 可以处理递归场景;
- 可以增加条件锁。
1.2.3 NS—LOCK
下面介绍Foundation
框架下的lock
类,其实NSLock
就是对pthread_mutex_t
的一个上层封装,使其更加面向对象:
NSLock
是对上面提到的pthread_mutex_t
普通锁用法的封装NSCondition
和NSConditionLock
是对上面提到的pthread_mutex_t
条件锁用法的封装NSRecursiveLock
是对上面提到的pthread_mutex_t
递归锁用法的封装。
2中方案验证下观点:
- 参考
GNUStep
的源码,地址找到Foundation
代码; - 通过
hopper
工具查看/System/Library/Frameworks/Foundation.framework
gnu
不是苹果源码,但是有很大的参考价值,截取部分代码如下
1 | @implementation NSLock |
再通过hopper
工具最后确认下
分别看下init
和lock
和unlock
1.2.3.1 NSLock
使用的demo地址
1.2.3.2 NSCondition
使用的demo地址
1.2.3.3 NSConditionLock
这个是对NSCondition
的封装,扩展了功能而已
主要是lockWhenCondition
和unlockWithCondition
,看下GNU
的代码
1 | - (void) lockWhenCondition: (NSInteger)value |
有了源码就很好理解了。
使用的demo地址
1.2.3.4 NSRecursiveLock
使用的demo地址
通过逆向验证了我们的观点,所以不用测试论速度的话肯定不如pthread_mutex_t
,毕竟objc_msgSend
也是要时间的。
1.2.4 Synchronized
使用方式很简单
1 | - (void)reduceCount { |
通过汇编代码看下synchronized
多线程等待的实现,代码修改见提交,断点在此处,拦截第二次断点,注意是第二次。
objc_sync_enter
-> os_unfair_recursive_lock_lock_with_options
-> _os_unfair_lock_lock_slow
-> __ulock_wait
1 | libsystem_kernel.dylib`__ulock_wait: |
发现到最后也是调用了syscall
,从_os_unfair_lock_lock_slow
开始和上面的os_unfair_lock
一样。
Synchronized
的源码是开源的在objc
的objc-sync.mm
文件中
1 | int objc_sync_enter(id obj) |
本质是对os_unfair_lock
的一个封装
1 | typedef struct os_unfair_recursive_lock_s { |
特点如下:
- 没有优先级反转的问题;
- 由于线程睡觉,当然唤醒也是要时间的,效率没有自旋锁高;
- 可以处理递归场景;
- synchronized的内部维护了一个map表,性能稍微其他的差些。
二、串行队列
我们要实现线程同步的根本原因是当多线程访问统一共享资源的时候会出现数据错乱的问题,那么只要能够保证多线程执行共享资源任务的时候能够顺序执行就可以了,那么串行队列也是一种方案。使用也很简单
1 |
|
三、信号量
使用如下:
1 | - (instancetype)init { |
- 通过
dispatch_semaphore_create
定好信号量的数量; dispatch_semaphore_wait
调用则self.semaphore
会减1,如果减1以后<=0 ,那么就会等待,知道>0;dispatch_semaphore_signal
会将self.semaphore
加1。
四、性能对比
关乎各种方案的性能对比yykit的作者做了个代码比较,地址
性能从高到低排序:
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
其实,不通过代码测试也能猜出个大概了:
- 自旋锁高于互斥锁
- 越接近底层的api肯定是更快的,所以c和GCD的会快于
oc
(上层封装)的;
五、推荐
开发中如果对性能要求比较高的话比较常见的选择:
- 常规锁的话,推荐使用
dispatch_semaphore
和pthread_mutex
; - 递归锁的话,推荐使用
pthread_mutex(recursive)
和NSRecursiveLock
(rac中用的是这个)
说明 本文的完整demo地址。