美文网首页
26.iOS底层学习之锁synchronized

26.iOS底层学习之锁synchronized

作者: 牛牛大王奥利给 | 来源:发表于2022-01-17 16:34 被阅读0次

    本篇提纲
    1、锁的简介
    2、锁的性能分析
    3、synchronized实现分析
    4、synchronized中的SyncData结构
    5、StripedMap的数据结构
    6、synchronized的执行流程

    1.锁的简介

    我们在使用多线程的时候,可能会遇到多个线程同时访问同一个数据,导致数据错乱和数据不安全的问题,所以就需要使用线程同步。而最常见的线程同步的方式就是加,以保证同一时间只有同一个线程在访问共享数据。

    2.锁的性能分析

    我们通过代码十万次循环,在循环中进行加锁,解锁的方式,来看一下各种锁对循环的时间影响。下面分别是真机和,模拟器运行的结果。

    真机是iPhone11 iOS 15,模拟器是iPhone11 iOS 15

    iPhone11 iPhone11模拟器

    通过运行结果可以看到@synchronized这种锁,在真机和模拟器上的表现差别很大,真机上性能要比模拟器好一些。而@synchronized也是我们最常用的锁,这篇文章主要就来研究下@synchronized的数据结构和内部的具体实现。

    3.synchronized实现分析

    我们通过符号断点的方式或者clang编译一下,跟踪到@synchronized对应的代码是这两句:objc_sync_enterobjc_sync_exit,我们在源码中看一下这两个方法的具体实现。

    • objc_sync_enter
    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不存在,那么会走objc_sync_nil 方法,进一步看,这个方法是一个宏定义,然后是空实现。也就是说,如果是obj为空,就什么都不做。

    • 如果obj存在,那么会走上边的if分支,这里边包括了一个新的结构体SyncData,我们后边会详细看下它的结构。

    • objc_sync_exit

    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方法在objc_sync_enterobjc_sync_exit中都有调用,而且这两个方法中的代码实现也非常的相似,都是去判断obj,为空就什么都不做,有值就去走id2data方法,我们来具体看下这个方法的实现。点进去发现大概有一百六十行左右,还挺多的。

    static SyncData* id2data(id object, enum usage why)
    {
      //1、传入object,从哈希表中获取数据
        //mutex_tt->os_unfair_lock 根据里面的文档翻译 是自旋锁
        spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    
    //传入object,从哈希表中获得SyncData的地址。
        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;
        SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    
        if (data) {
            fastCacheOccupied = YES;
    
            if (data->object == object) {
                // Found a match in fast cache.
                uintptr_t lockCount;
    
                result = data;
              //2、在当前线程中的tls中寻找
                lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
                if (result->threadCount <= 0  ||  lockCount <= 0) {
                    _objc_fatal("id2data fastcache is buggy");
                }
    
                switch(why) {
                case ACQUIRE: {
                  //锁+1
                    lockCount++;
                    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                  //再存储到tls中
                    break;
                }
                case RELEASE:
                    lockCount--;
                    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                   //锁的个数减完之后为0了
                    if (lockCount == 0) {
                        // remove from fast cache
                      //删除局部存储
                        tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                        // atomic because may collide with concurrent ACQUIRE
                        //对SyncData对象的threadCount进行-1,因为当前线程中的对象已经解锁了
                        OSAtomicDecrement32Barrier(&result->threadCount);
                    }
                    break;
                case CHECK:
                    // do nothing
                    break;
                }
    
                return result;
            }
        }
    #endif
    
      //3、TLS中没找到,在各自线程的缓存中查找
        // 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;
                if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                    _objc_fatal("id2data cache is buggy");
                }
                    
                //这个部分的执行和在TLS中的类似
                switch(why) {
                case ACQUIRE:
                    item->lockCount++;
                    break;
                case RELEASE:
                    item->lockCount--;
                    if (item->lockCount == 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;
            }
        }
    
        // 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.
        
        //加锁,保证下面代码到解锁部分的线程安全
        lockp->lock();
        {
            SyncData* p;
            SyncData* firstUnused = NULL;
    //4、遍历syncList,如果无法遍历,证明当前object的list不存在,需要创建。
            for (p = *listp; p != NULL; p = p->nextData) {
                //查到了对象
                if ( p->object == object ) {
                    result = p;
                    // atomic because may collide with concurrent RELEASE
                    //对threadCount+1
                    OSAtomicIncrement32Barrier(&result->threadCount);
                    //跳转至done
                    goto done;
                }
                
                //没查到 记录下object的位置
                if ( (firstUnused == NULL) && (p->threadCount == 0) )
                    firstUnused = p;
            }
        
            // 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;
                result->threadCount = 1;
                goto done;
            }
        }
    
        // 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对象 并且添加到syncList中
        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_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;
    }
    
    • SUPPORT_DIRECT_THREAD_KEYS:支持线程占存,线程占存TLS

    • TLS,线程局部存储(Thread Local Storage,TLS),是操作系统为线程单独提供的私有空间,通常只有有限的容量。

    • ACQUIRE在方法objc_sync_enter传入的值,对lockCount进行+1操作,并存储。

    • RELEASE在方法objc_sync_exit传入的值,对lockCount进行-1操作,并进一步判断lockCount的值是不是为0,如果为0,对threadCount进行-1操作。

    • done对list中找到的object而在TLS或者cache没有找到的对象,进行TLS存储,或者cache存储,并且进行一些错误判断。

    • 链表头插法


      链表头插法演示.jpg

    4.synchronized中的SyncData结构

    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中又有一个struct SyncData* nextData;相同类型的指向下一个节点的一个next,所以这是一个单向链表,节点中存储了下一个节点的地址。
    • threadCount使用block块的线程数
    • recursive_mutex_t递归锁,底层还是os_unfair_lock。

    5.StripedMap的数据结构

    我们通过代码看到SyncData是从LIST_FOR_OBJ中取出来的,

        SyncData **listp = &LIST_FOR_OBJ(object);
    

    进一步看LIST_FOR_OBJ它的定义是

    #define LIST_FOR_OBJ(obj) sDataLists[obj].data
    

    是一个宏,而sDataLists是一个静态表

    static StripedMap<SyncList> sDataLists;
    struct SyncList {
        SyncData *data;
        spinlock_t lock;
        constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };
    

    StripedMap是哈希类型,所以sDataLists是一张静态哈希表,内部存储SyncData,而SyncData本身又是单链表,所以StripedMap哈希表+单链表的结构。

    StripedMap解决哈希冲突的方法是通过拉链法,就是如果计算的下标已经存储了内容,那么会存储到SyncData`的next中,如果next还有内容,会继续往下找,直到找到可以存储的位置。

    StripedMap结构示意图

    结构示意.jpg
    class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
        enum { StripeCount = 8 };
    #else
        enum { StripeCount = 64 };
    #endif
    };
    

    在真机分配了8个空间,模拟器分配64个。当把模拟器修改成1后,不同的对象来到id2data时,通过打印可以看到,当冲突了会存到冲突位置的nextData中。

    冲突处理示意

    6.Synchronized的执行流程

    通过上面的讨论,可以整理出以下流程。
    1、调用@ synchronized(object){}时,相当于调用了方法objc_sync_enterobjc_sync_exit
    2、在objc_sync_enter方法中和objc_sync_exit方法中首先都是进行对传入的object判断,如果为nil就什么都不做;
    如果存在,那么objc_sync_enterobjc_sync_exit都会调用方法id2data只不过方法objc_sync_enter中传的参数是ACQUIRE,而objc_sync_exit传的是RELEASE,这正好对应了id2data方法中switch分支的处理。
    3、id2data中的逻辑是这样:

    • 3.1 首先判断是否支持TLS,如果支持从TLS中查找相关的object存储信息,查到了,入到switch(why)的分支判断,如果是ACQUIRE,那么锁lockCount+1,然后更新存储,返回result
      如果是RELEASE,那么锁lockCount-1,然后更新存储,再进一步判断lockCount是不是0,如果为0,threadCount-1操作,然后更新存储。

    • 3.2 如果从TLS中没查到,那么查SyncCache缓存,进行缓存的遍历,如果查到了这个对象的缓存,进入到switch(why)的分支判断,如果是ACQUIRE,那么锁lockCount+1,然后更新存储,返回result
      如果是RELEASE,那么锁lockCount-1,然后更新存储,再进一步判断lockCount是不是0,如果为0,threadCount-1操作,然后更新存储。

    • 3.3 如果缓存也没查到,那么去遍历object所在的listp中查找,如果查到了,进行threadCount的处理,并且跳转到donedone的操作是,先进行上面查找读取的解锁,然后进行简单的错误判断。如果支持TLS,那么把信息更新到TLS中进行存储(这样下次再来的时候,第一步就可以查到了),如果不支持,那么更新到cache中,下次进来的时候第二步就可以查到了。然后返回result

    • 3.4 如果list中也没查到,那么创建一个新的SyncData对象 并使用头插法插入到链表中(这样下次再来到list就可以查到返回了,然后执行list往缓存存储的流程),并且返回result

    相关文章

      网友评论

          本文标题:26.iOS底层学习之锁synchronized

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