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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容

  • 前言 上一篇文章研究完了GCD相关的底层原理,现在我们开始探索锁的底层原理。众所周知,锁分为两大类:自旋锁&互斥锁...
    冼同学阅读 752评论 0 2
  • 一、性能分析 网上很多对比八大锁性能的文章,时间大部分比较早。苹果对某些锁内部进行了优化。这篇文章找中会以10万次...
    HotPotCat阅读 1,167评论 1 4
  •   iOS中各种锁性能对比,建立一个10万次的循环,加锁、解锁,对比前后时间差得到其耗时时间。以下是真实的测试结果...
    spyn_n阅读 1,080评论 0 2
  • 锁的种类 借用网上的一张有关锁性能的对比图,如下所示: 从上图中我们可以看出来,锁大概可以分为以下几种: 1.:在...
    含笑州阅读 1,006评论 0 0
  • @synchronized是比较常见的线程间同步锁,其使用相当简单: 可在上述代码synchronized行断点,...
    大成小栈阅读 1,171评论 0 2