在多线程运用中,我们经常会遇到资源竞争问题。比如多个线程同时往一个文件写入,这种情况是不被允许的,会造成安全隐患。正常的做法是一个线程写完后,下一个线程才能写入。而在多线程中,这一项技术叫做线程同步技术。
那么在iOS中,有哪些方案可以使用?
- pthread_mutex
- NSLock、NSRecursiveLock
NSCondition、NSConditionLock
os_unfair_lock(替换被废弃的OSSpinLock,iOS 10+)
- @synchronized
- dispatch_semaphore
- 串行队列
0x01 pthread_mutex
也叫互斥锁,以休眠来等待锁被解开。以经典的“存取钱”为例,我们看下如何使用pthread_mutex控制线程同步问题。
1 | static pthread_mutex_t g_mutex; |
需要注意的是,使用pthread_mutex需要手动进行销毁。
如下代码,我们进行递归调用的时候会发生什么?
1 | - (void)recursiveTest { |
由于递归调用,第一次进入recursiveTest的时候调用pthread_mutex_lock加锁,然后在临界区(lock与unlock之间的代码称为临界区)里又调用了自己,然而之前锁已经加上了,所以整理等待解锁,但是等待解锁了就不能继续往下执行,就造成不能执行到pthread_mutex_unlock解锁,这样就导致锁一直不能放开,所以造成了死锁。
解决这样的问题,我们可以使用递归锁。初始化属性的时候把PTHREAD_MUTEX_NORMAL改为PTHREAD_MUTEX_RECURSIVE。
1 | - (void)initLock { |
我们有时需要加锁的前提是满足某些条件,比如现在有这么一个需求,同时依次调用三个接口,调用完毕后合并这三个接口得到的数据,我们可以实现为
1 | static pthread_mutex_t g_mutex; |
0x02 NSLock、NSRecursiveLock、NSCondition、NSConditionLock
这几个Lock都是基于mutex的封装。这里只对NSConditionLock做代码示例,其他的api已经够简单明了了,所以不做详细示例。
1 | - (void)initLock { |
NSConditionLock可以设置多任务间的依赖关系。
0x03 os_unfair_lock
作为替代OSSpinLock的方案,曾一度以为也是自旋锁,但是查看汇编后调用了睡眠函数。
1 | - (void)viewDidLoad { |
查看汇编
1 | libsystem_platform.dylib`os_unfair_lock_lock: |
0x04 @synchronized
@synchronized也是对mutex的封装。
1 | - (void)drawMoney { |
使用clang转换代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 ViewController.m
1 | static void _I_ViewController_drawMoney(ViewController * self, SEL _cmd) { |
转换后的代码,我们可以看到@synchronized内部其实调用了objc_sync_enter和objc_sync_exit。
1 | int objc_sync_enter(id obj) |
为什么说是对mutex的封装,我们看下SyncData的定义
1 | typedef struct SyncData { |
我们定义属性的时候,对其原子性经常定义为nonatomic,因为性能等原因很少用到atomic。我们知道如果对OC对象使用atomic,会自动在其getter\setter方法里加锁。我们定义属性如下
1 | @property (atomic, strong) NSArray *datas; |
我们汇编查看其setter方法,内部调用了objc_setProperty_atomic
1 | testData`-[ViewController setDatas:]: |
objc_setProperty_atomic内部使用了os_unfair_lock加锁
1 | void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) |
对于像NSMutableArray和NSMutableDictionary,使用atomic虽然对其setter\getter方法进行了加锁,但是在多线程环境下使用addObject:或removeObjectAtIndex:这类对对象内部数据操作的方法,其实还是不安全的。也就是说加锁仅限于读取对象(NSArray *array = self.datas;)和修改对象(self.datas = @[].mutableCopy),但这个对象其内部的方法操作是不安全的。
0x05 dispatch_semaphore和串行队列
在多线程篇里已经提过,不重复介绍。