26.iOS底层学习之锁synchronized

本篇提纲
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 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 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)