在多线程运用中,我们经常会遇到资源竞争问题。比如多个线程同时往一个文件写入,这种情况是不被允许的,会造成安全隐患。正常的做法是一个线程写完后,下一个线程才能写入。而在多线程中,这一项技术叫做线程同步技术。
那么在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和串行队列
在多线程篇里已经提过,不重复介绍。