iOS多线程可能造成共享资源的竞争,使用锁可以很好的解决这一问题,iOS的锁有很多种,从性能上看@synchronized似乎没啥竞争力,来自# 不再安全的 OSSpinLock
但是我们为什么还要用这厮呢,因为用起来简单啊!!!!
@synchronized (person) {
//do something
}
源码分析
打开always show disassembly运行代码
通过objc_retain和objc_release可以看到@synchronized和对象强引用,验证一下
int main(int argc, char * argv[]) {
JPerson *person = [[JPerson alloc] init];
NSLog(@"--%lu",CFGetRetainCount((__bridge CFTypeRef)(person)));
@synchronized (person) {
NSLog(@"%@",person);
NSLog(@"--%lu",CFGetRetainCount((__bridge CFTypeRef)(person)));
}
NSLog(@"--%lu",CFGetRetainCount((__bridge CFTypeRef)(person)));
return 0;
}
objc_sync_enter
在objc4-781.2搜索objc_sync_enter
- 如果
obc为空就执行objc_sync_nil,进一步查看发现什么也没执行 obc不为空就获取对象SyncData* data的data->mutext加锁 这里的重点应该在SyncData和id2data
SyncData
这是一个单链表节点
- nextData:指向下一个节点的指针
- object:包装后的obj
- threadCount:有多少个线程访问obj
- mutex:recursive_mutext_t类型的递归互斥锁
id2data
这里代码比较多,关注点太多容易分散精力,我们只重点关注非缓存的情况系统是如何处理的
点进去看看
这时的重点来到了StripedMap<SyncList>
可以看到StripedMap是一个数组结构,用来存储SyncList
- 如果找到了
obj对应的SyncData跳转到done执行 - 如果没找到
obj对应的SyncData但是找到了空节点,那么将obj赋值给空节点 - 如果都没找到,那么创建一个插入
SyncList的头部
id2data简单总结
如图:有一个全局的长度为8的数组StripedMap(这个数组是不需要扩容的),数组里存储的是SyncList指针,SyncList里面存储SyncData节点指针,SyncData节点存储指向下一个SyncData节点指针,形成单链表结构。
为了提升访问速度,苹果设计了两级缓存TLS和SyncCache,下面我们具体看一下
TLS(Thread Local Storage)线程本地存储
TLS可以认为是线程私有存储空间
tls_get_direct:根据key从TLS字典读取值tls_set_direct:根据key、value存储到TLS字典
可以看到如果没有从快速缓存TLS中查找到结果,done后面代码执行也会存储一份进去
SyncCache
操作很简单,就是遍历数组匹配object
SyncCacheItem结构体包含SyncData指针- 当前线程访问次数
lockCount
SyncCache结构体包含- 存储
SyncCacheItem的数组list - 数组长度
allocated - 当前已经存储元素数量
used
- 存储
fetch_cache
_objc_pthread_data结构
_objc_fetch_pthread_data
从这里我们看到了两个熟悉的身影tls_get和tls_set,创建的_objc_pthread_data结构体也是存储在了TLS中
objc_sync_exit
只做了一个解锁操作,创建的SyncData并没有删除
注意
我们是通过传入的obj地址作为标识来操作SyncData的,如果obj地址发生变化那是会出现问题的,例如
简单理解假如t1、t2两个线程要访问person,person初始值为a0
- t1获取到
person地址为a0,加锁 - t2获取到
person地址也为a0,等待解锁 - t1操作完之后
person地址变为a1,a0被释放,解锁,t2可访问 - t2操作完之后
person地址变为a2,a0又被释放,重复释放造成崩溃
所以我们一般传入对属性所在对象的self,这样虽然一定程度上保证了线程安全,但是影响范围扩大
补充两个小例题加深理解
1、 请问1和end谁先输出??
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_async(globalQueue, ^{
NSObject *obj = [NSObject new];
@synchronized (obj) {
//这里是异步执行
dispatch_async(globalQueue, ^{
@synchronized (obj) {
NSLog(@"1--%@",[NSThread currentThread]);
}
});
sleep(3);
NSLog(@"end--%@",[NSThread currentThread]);
}
});
答案是先输出end后输出1,因为两个线程都要对obj加锁,第二个线程一定要等待第一个线程解锁之后才会执行
2、 修改一下:请问1和end谁先输出??
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_async(globalQueue, ^{
NSObject *obj = [NSObject new];
@synchronized (obj) {
//这里是同步执行
dispatch_sync(globalQueue, ^{
//通过TLS一级缓存获取到了syncData
@synchronized (obj) {
sleep(3);
NSLog(@"1--%@",[NSThread currentThread]);
}
});
NSLog(@"end--%@",[NSThread currentThread]);
}
});
这个时候就是先输出1后输出end了,同一个线程可以对同一个syncData多次加锁,因为是同步执行所以当前线程先执行任务1
3、 修改一下:请问1和end谁先输出??
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_async(globalQueue, ^{
NSObject *obj = [NSObject new];
@synchronized (obj) {
//一级缓存替换成了obj1
NSObject *obj1 = [NSObject new];
@synchronized (obj1) {
NSLog(@"obj1");
}
dispatch_sync(globalQueue, ^{
//这里是通过二级缓存取到的syncData
@synchronized (obj) {
sleep(3);
NSLog(@"1--%@",[NSThread currentThread]);
}
});
NSLog(@"end--%@",[NSThread currentThread]);
}
});
因为存在同步执行,此时obj1一定先输出,1仍然在end之前输出