iOS多线程之@synchronized探索

1,346 阅读4分钟

iOS多线程可能造成共享资源的竞争,使用锁可以很好的解决这一问题,iOS的锁有很多种,从性能上看@synchronized似乎没啥竞争力,来自# 不再安全的 OSSpinLock

lock_benchmark.png

但是我们为什么还要用这厮呢,因为用起来简单啊!!!!

    @synchronized (person) {
        //do something
    }

源码分析

打开always show disassembly运行代码

image.png

image.png

通过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;
}

image.png

objc_sync_enter

objc4-781.2搜索objc_sync_enter

image.png

  • 如果obc为空就执行objc_sync_nil,进一步查看发现什么也没执行
  • obc不为空就获取对象SyncData* datadata->mutext加锁 这里的重点应该在SyncDataid2data

SyncData

image.png

这是一个单链表节点

  • nextData:指向下一个节点的指针
  • object:包装后的obj
  • threadCount:有多少个线程访问obj
  • mutex:recursive_mutext_t类型的递归互斥锁

id2data

image.png

这里代码比较多,关注点太多容易分散精力,我们只重点关注非缓存的情况系统是如何处理的

image.png

点进去看看

image.png

这时的重点来到了StripedMap<SyncList>

image.png

image.png

可以看到StripedMap是一个数组结构,用来存储SyncList

image.png

  • 如果找到了obj对应的SyncData跳转到done执行
  • 如果没找到obj对应的SyncData但是找到了空节点,那么将obj赋值给空节点
  • 如果都没找到,那么创建一个插入SyncList的头部

image.png

id2data简单总结

0b265c3307354cb9a79a75638462335f_tplv-k3u1fbpfcp-watermark.png

如图:有一个全局的长度为8的数组StripedMap(这个数组是不需要扩容的),数组里存储的是SyncList指针,SyncList里面存储SyncData节点指针,SyncData节点存储指向下一个SyncData节点指针,形成单链表结构。

为了提升访问速度,苹果设计了两级缓存TLSSyncCache,下面我们具体看一下

TLS(Thread Local Storage)线程本地存储

TLS可以认为是线程私有存储空间

image.png

  • tls_get_direct:根据key从TLS字典读取值
  • tls_set_direct:根据key、value存储到TLS字典

image.png

可以看到如果没有从快速缓存TLS中查找到结果,done后面代码执行也会存储一份进去

SyncCache

image.png

操作很简单,就是遍历数组匹配object

image.png

  • SyncCacheItem结构体包含
    • SyncData指针
    • 当前线程访问次数lockCount
  • SyncCache结构体包含
    • 存储SyncCacheItem的数组list
    • 数组长度allocated
    • 当前已经存储元素数量used

fetch_cache

image.png

_objc_pthread_data结构

image.png

_objc_fetch_pthread_data

image.png

从这里我们看到了两个熟悉的身影tls_gettls_set,创建的_objc_pthread_data结构体也是存储在了TLS中

二级缓存

objc_sync_exit

image.png

只做了一个解锁操作,创建的SyncData并没有删除

注意

我们是通过传入的obj地址作为标识来操作SyncData的,如果obj地址发生变化那是会出现问题的,例如

image.png

简单理解假如t1、t2两个线程要访问personperson初始值为a0

  • t1获取到person地址为a0,加锁
  • t2获取到person地址也为a0,等待解锁
  • t1操作完之后person地址变为a1,a0被释放,解锁,t2可访问
  • t2操作完之后person地址变为a2,a0又被释放,重复释放造成崩溃

所以我们一般传入对属性所在对象的self,这样虽然一定程度上保证了线程安全,但是影响范围扩大

补充两个小例题加深理解

1、 请问1end谁先输出??
    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]);
        }
    });

image.png

答案是先输出end后输出1,因为两个线程都要对obj加锁,第二个线程一定要等待第一个线程解锁之后才会执行

2、 修改一下:请问1end谁先输出??
    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]);
        }
    });

image.png

这个时候就是先输出1后输出end了,同一个线程可以对同一个syncData多次加锁,因为是同步执行所以当前线程先执行任务1

3、 修改一下:请问1end谁先输出??
    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]);
        }
    });

image.png

因为存在同步执行,此时obj1一定先输出,1仍然在end之前输出

参考文章

抛开性能,谈谈不该用@Synchronized的原因

iOS多线程锁之@synchronized原理分析

IOS - @synchronized详解

不再安全的 OSSpinLock