美文网首页
@synchronized底层探索&其他锁

@synchronized底层探索&其他锁

作者: 猿人 | 来源:发表于2020-11-17 22:50 被阅读0次

    锁的性能排行

    锁的性能排行.png

    锁的归类

    自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

    互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
    上图中属于互斥锁的有:

    • NSLock
    • pthread_mutex
    • @synchronized

    条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源分配到了,条件锁打开,进程继续运行
    上图中属于条件锁的有:

    • NSConfition
    • NSConditionLock

    递归锁:就是同一线程可以加锁N次而不会引发死锁
    上图中属于递归锁的有

    • NSRecursiveLock
    • pthread_mutex(recursive)

    信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间的互斥。

    其实基本的锁就包括了三类,自旋锁 互斥锁 读写锁,其他的比如条件锁,递归锁,信号量都是上层的封装和实现!

    引用:百度百科读写锁

    @synchronized

    对于@synchronized 的使用大家都不陌生,但是它的底层实现是怎样的呢?通过底层分析我们又能得到什么新的发现?下面废话不多说直接探寻其底层。

    如何进行探索(知道的可略过直接去看底层源码分析)
    1、 dome 准备
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        self.ticketCount = 15;
        [self  testSaleTicket];
    }
     
    - (void)testSaleTicket{
        ///窗口 1
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 5; i++) {
                [self saleTicket];
            }
        });
        ///窗口 2
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 5; i++) {
                [self saleTicket];
            }
        });
        ///窗口 3
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 10; i++) {
                [self saleTicket];
            }
        });
    }
    
    - (void)saleTicket{
    //     @synchronized (self) {
            
            if (self.ticketCount > 0) {
                self.ticketCount--;
                sleep(0.1);
                NSLog(@"当前余票还剩:%ld张",self.ticketCount);
                
            }else{
                NSLog(@"当前车票已售罄");
            }
    //     }
     }
    

    在没有考虑到线程安全的情况我们运行其任务


    截屏2020-11-17 下午2.25.41.png
    • 这明显这票数 有问题 票池 抽了疯,不管三七二一的瞎胡 扯的反馈。

    当用上了 @synchronized 完美的解决了问题


    截屏2020-11-17 下午2.36.29.png
    2、如何分析synchronize

    那肯定是符号断点,clang了
    首先符号断点打开 Debug -> Debug Workflow -> Always Show Disassembly

    符号断点打开
    将断点 打到 @synchronized 并运行 在汇编里我们找到了 两个很重要的线索
    汇编线索1 汇编线索2
    • objc_sync_enter 函数
    • objc_sync_exit 函数

    我们 到此先记住这两个函 这是可疑的两个函数 下面在clang一下 @synchronized 看clang编译器是怎样实现的。
    在main函数中写一个 @synchronized


    截屏2020-11-17 下午3.02.33.png

    通过命令 得到 mian.cpp

    clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk main.m
    
    截屏2020-11-17 下午3.16.03.png
    • 通过 clang我们 也发现了上面的两个函数 是一模一样的,证明上面的两个函数 正是 我们要研究的。

    找到 objc_sync_enter 和 objc_sync_exit 所在的库
    下符号断点


    下符号断点找到所在的库
    • 此时我们知道了 objc_sync_enter 函数 是在 libojc 中掉起的。


      截屏2020-11-17 下午3.25.43.png
    • objc_sync_exit 函数 也是由 libobjc 中调起的

    到这里我们也就知道了@synchronized 底层 是由 objc_sync_enter 和objc_sync_exit 两个重要的函数组合而成 他们来自 libobjc 动态库。也就找到 程序的入口 分析的入口。

    objc_sync_enter&objc_sync_exit 函数分析

    找到objc4源码 并定位到当前函数

    int objc_sync_enter(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
    
        if (obj) {
    ///重点   
            SyncData* data = id2data(obj, ACQUIRE);
            ASSERT(data);
            data->mutex.lock();
        } else {
            // @synchronized(nil) does nothing
            if (DebugNilSync) {
                _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
            }
            objc_sync_nil();
        }
    
        return result;
    }
    
    • 从这里可以看到 如果obj为真的话 通过id2data函数 获取一个SyncData 对象,并将此对象里面的 mutex 的属性 上锁

    我们看 SyncData 类型

    typedef struct alignas(CacheLineSize) SyncData {
        struct SyncData* nextData; 
        DisguisedPtr<objc_object> object;
        int32_t threadCount;  // number of THREADS using this block
        recursive_mutex_t mutex;
    } SyncData;
    
    • 可以看到SyncData是一个结构体,里面包含一个指向下一个SyncData的指针nextData,可以看出SyncData是链表中的一个节点。
    • 包含object 将其类型进行了伪装,其实它就是我们传进来的 object
    • 里面还有一个 threadCount,通过注释我们可以详细的看到 使用此块的线程数。
    • 还有一把锁,从这把锁的定义来看 它是一个递归互斥类型

    来到 id2data函数看里面如何获取到SyncData的对象的
    由于函数太长我们拆分几大块来看

    第一步: 判断是否支持tls缓存,从tls缓存中获取obj的相关信息

    static SyncData* id2data(id object, enum usage why)
    {
        ///跟当前对象关联的所有的被锁线程中的锁任务的状态
        spinlock_t *lockp = &LOCK_FOR_OBJ(object);
        ///跟当前对象关联的所有的被锁线程数据
        SyncData **listp = &LIST_FOR_OBJ(object);
        SyncData* result = NULL;
    
    #if SUPPORT_DIRECT_THREAD_KEYS
        //检查每个线程单条目快速缓存是否匹配对象
        // Check per-thread single-entry fast cache for matching object
        ///默认没找到
        bool fastCacheOccupied = NO;
        ///从线程中读取数据  (tls: (Thread Local Storage) 线程本地存储)
        SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
        if (data) {
       /// 找到了 设置为 YES
            fastCacheOccupied = YES;
             ///如对象是传入的对象
            if (data->object == object) {
                // Found a match in fast cache. ///从快速缓存中找到
                uintptr_t lockCount;
                ///返回值赋值
                result = data;
                /// 当前线程 被锁了 几回 如当前线程递归调用锁
                lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
                /// 如果使用此块儿的线程总数 或者 当前线程被锁次数 都小于等于0 那么这时候bug
                if (result->threadCount <= 0  ||  lockCount <= 0) {
                    _objc_fatal("id2data fastcache is buggy");
                }
    
                switch(why) {
                case ACQUIRE: {///进行中
                    lockCount++;///将当前线程被锁次数+1
                     ///更新线程缓存的任务数
                    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                    break;
                }
                case RELEASE:/// 释放中
                    lockCount--;///将当前线程被锁次数 -1
                    ///更新线程缓存的任务数
                    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                    /// 如当前线程被锁的任务都执行完了 那么 释放线程缓存
                    if (lockCount == 0) {
                        // remove from fast cache
                        tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                        // atomic because may collide with concurrent ACQUIRE
                        OSAtomicDecrement32Barrier(&result->threadCount);
                    }
                    break;
                case CHECK:///啥都不干 应该是预留
                    // do nothing
                    break;
                }
               /// 返回
                return result;
            }
        }
    #endif
    
    

    tls,Thread Local Storage,线程局部储存,它是操作系统为线程单独提供的私有空间,通常只有有限的容量。
    百度百科:线程局部存储

    • tls读取数据,如果找到了并且和当前被锁对象一样,获取当前 线程 被锁几回的lockCount
    • 如当前 是 ACQUIRE (也就是 objc_sync_enter调用的)那说明在当前线程上对象又被锁了一次,锁的次数加+1。 更新tls中存储的obj信息。并返回
    • 如当前 是 RELEASE (也就是 objc_sync_exit)发起的调用,那说明 在当前线程上的被锁任务应该 -1 。更新tls中存储的obj信息。并返回
    • 如在tls中并未找到,那么进入第二步

    第二步:在线程缓存中SyncCache中查找是否存在obj的数据信息

    #endif
      
       /// //检查已拥有锁的每个线程缓存是否匹配对象
        // Check per-thread cache of already-owned locks for matching object
        SyncCache *cache = fetch_cache(NO);
        if (cache) {
            unsigned int i;
            ///遍历所有的拥有锁任务的线程 在线程缓存中
            for (i = 0; i < cache->used; i++) {
                SyncCacheItem *item = &cache->list[i];
                ///判断线程中的对象并不是我们传进的对象 跳过本次循环
                if (item->data->object != object) continue;
    
                // Found a match. ///找到了当前对象所关联的线程。
                result = item->data;
                /// 如果 当前对象所关联的 线程总数 小于等于0
                /// 或 当前对象所关联的线程 锁任务的个数小于等于0 程序bug
                if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                    _objc_fatal("id2data cache is buggy");
                }
                    
                switch(why) {
                case ACQUIRE: ///进行中
                    item->lockCount++; ///当前线程任务数+1
                    break;
                case RELEASE:///释放中
                    item->lockCount--; ///当前线程任务数 -1
                    if (item->lockCount == 0) { ///当前线程加锁任务 为 0 那么 移除缓存
                        // remove from per-thread cache
                        cache->list[i] = cache->list[--cache->used];
                        // atomic because may collide with concurrent ACQUIRE
                        OSAtomicDecrement32Barrier(&result->threadCount);
                    }
                    break;
                case CHECK:
                    // do nothing ///啥都不干
                    break;
                }
    
                return result; ///返回
            }
        }
    
    
    • 从线程缓存中遍历查找 和当前传进的对象对应的线程缓存。 如找到了 拿到 当前线程的缓存对象SyncCacheItem

    • 如当前 是 ACQUIRE (也就是 objc_sync_enter调用的)那说明在当前线程上对象又被锁了一次,锁的次数(lockCount)加+1。

    • 如当前 是 RELEASE (也就是 objc_sync_exit)发起的调用,那说明 在当前线程上的被锁任务次数标识(lockCount)应该 -1 。 如果当前线程上的任务数为0 那么移除线程缓存

    • 如在线程缓存中也没有那么进入第三步

    这里看一下 缓存结构(SyncCache )及 缓存对象结构(SyncCacheItem)

    ///线程缓存
    typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
    } SyncCache;
    ///缓存对象item
    typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked >this block
    } SyncCacheItem;
    

    第三步:使用列表 sDataLists中查找对象,并做处理

        // Thread cache didn't find anything.
        // Walk in-use list looking for matching object
        // Spinlock prevents multiple threads from creating multiple 
        // locks for the same new object.
        // We could keep the nodes in some hash table if we find that there are
        // more than 20 or so distinct locks active, but we don't do that now.
        ///线程缓存没有找到任何东西。,需要遍历每个线程,沿着nextData递归查找
        ///上锁
        lockp->lock();
    
        {
            SyncData* p;
            SyncData* firstUnused = NULL;
          
            ///遍历跟当前object相关的 所有线程任务
            for (p = *listp; p != NULL; p = p->nextData) {
                ///再次判断是否是当前 object
                if ( p->object == object ) {
                    result = p;//找到赋值
                     //原子操作 可能会和 并发 释放 冲突
                    // atomic because may collide with concurrent RELEASE
                    OSAtomicIncrement32Barrier(&result->threadCount);
                    goto done;//跳出
                }
                ///没找到与当前objc关联的锁任务线程 更新第一个没有使用的线程
                if ( (firstUnused == NULL) && (p->threadCount == 0) )
                    firstUnused = p;
            }
            //当前没有与对象关联的SyncData
            // no SyncData currently associated with object
            if ( (why == RELEASE) || (why == CHECK) )
                goto done;
        
            //发现一个未使用的,就使用它
            // an unused one was found, use it
            if ( firstUnused != NULL ) {
                result = firstUnused;
                result->object = (objc_object *)object;///将当前对象存入 object
                result->threadCount = 1;//只有一个线程加锁
                goto done;
            }
        }
    
        //分配一个新的SyncData并添加到列表
        // Allocate a new SyncData and add to list.
        // XXX allocating memory with a global lock held is bad practice,
        // might be worth releasing the lock, allocating, and searching again.
        // But since we never free these guys we won't be stuck in allocation very often.
        //分配一个新的SyncData并添加到列表。
        // XXX用持有的全局锁分配内存是不好的做法,
        //可能值得释放锁、重新分配和搜索。
        //但由于我们从来没有释放这些,我们就不会经常陷入分配的困境。
        posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
        result->object = (objc_object *)object;
        result->threadCount = 1;
        new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
        result->nextData = *listp;
        *listp = result;
        
     done:
        lockp->unlock();
        if (result) {
            // Only new ACQUIRE should get here.
            // All RELEASE and CHECK and recursive ACQUIRE are 
            // handled by the per-thread caches above.
            if (why == RELEASE) {
                // Probably some thread is incorrectly exiting 
                // while the object is held by another thread.
                return nil;
            }
            if (why != ACQUIRE) _objc_fatal("id2data is buggy");
            if (result->object != object) _objc_fatal("id2data is buggy");
    
    #if SUPPORT_DIRECT_THREAD_KEYS
            if (!fastCacheOccupied) {
                // Save in fast thread cache ///存入 tls
                tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
            } else 
    #endif
            {
                // Save in thread cache //存入线程缓存
                if (!cache) cache = fetch_cache(YES);
                cache->list[cache->used].data = result;
                cache->list[cache->used].lockCount = 1;
                cache->used++;
            }
        }
    
        return result;
    }
    
    • 在列表sDataLists中 查找,就需要对查找过程加锁防止多线程查找导致数据异常。使用列表 sDataListsSyncData又做了一层封装,元素是一个结构体SyncList.

    这里我们回到最上面看一下 *listp

     ///跟当前对象关联的所有的被锁线程中的锁任务的状态
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
     ///跟当前对象关联的所有的被锁线程数据
    
    SyncData **listp = &LIST_FOR_OBJ(object);
    ///进入 LOCK_FOR_OBJ 发现是一个宏
    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    ///sDataLists 是一个静态的 map 泛型为 SyncList 也就是key为object指针,value为SynLlist
    static StripedMap<SyncList> sDataLists;
    
    struct SyncList {
    SyncData *data;
    spinlock_t lock;
    
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };
    
    • 如找到,解锁,将数据写入tls ,写入线程缓存,并返回数据
    • 如未找到,创建一个新的SyncData放入sDataLists中,并存入tls线程缓存中然后返回

    看完了objc_sync_enter 下面看 objc_sync_exit 锁的释放

    // End synchronizing on 'obj'. ///根据obj结束 加锁
    // Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
    int objc_sync_exit(id obj)
    {
        int result = OBJC_SYNC_SUCCESS;
        
        if (obj) {
            SyncData* data = id2data(obj, RELEASE); 
            if (!data) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            } else {
                bool okay = data->mutex.tryUnlock();
                if (!okay) {
                    result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
                }
            }
        } else {
            // @synchronized(nil) does nothing
        }
        
    
        return result;
    }
    

    上面我们已经统一的分析了id2data函数,这里传进的是RELEASE
    下面总结objc_sync_exit 函数 的id2data做了什么事情

    • 1、先从tls缓存中查找,如果找到,对锁的计数(lockCount)减1,更新缓存中的数据,如果当前对象对应的锁计数为0了,直接将其从tls缓存中删除。未找到进入2

    • 2、从线程缓存SyncCache中查找,如果找到,对锁的计数减1,更新缓存中的数据,如果当前对象对应的锁计数为0了,直接将其从线程缓存SyncCache中删除。未找到进入3

    • 3、从sDataLists查找,找到的话,直接将其置为nil

    总结

    • synchronized底层我们看到了有对同一条线程上的 加锁任务计数lockCount。有 使用此块 的线程数的统计 threadCount
      还看到了 SyncData对象中 的 recursive_mutex_t
      由此可以下结论synchronized 是一把 递归互斥锁,
    • synchronized 进入代码块的入口为objc_sync_enter,出口为objc_sync_enter
    • 如果@synchronized(nil)传入的为nil那么锁将不起任何作用
      核心处理逻辑:
    • 如支持tls缓存,就从tls缓存中查找对象SyncData,找到对lockCount进行相应操作。
    • 如果不支持tls缓存,或者从tls缓存中未找到,就从线程缓存SyncCache中查找,同样如找到 就对lockCount进行相应操作。
    • 如缓存中没有找到,就从sDataLists链表中查找,找到后进行相关操作,并写入tls缓存和线程缓存SyncCache.
    • 都没找到,创建一个节点,将对象锁SyncData插入sDataLists,并写入缓存.

    相关文章

      网友评论

          本文标题:@synchronized底层探索&其他锁

          本文链接:https://www.haomeiwen.com/subject/hggabktx.html