OC底层探索24-synchronize锁的原理

1、八大锁效率

  • 八大锁分别:
    • 自璇所:OSSpinLock。在iOS10以后该锁被重写,会在堵塞时进行休眠
    • 互斥锁:NSLock、NScondition、NSRecursiceLock、NSConditionLock、@synchronize;以及更加偏底层:pthread_mutex、pthread_mutex(recursive);

2、synchronize探索入口

所有底层的探索都需要一个切入点,像这样的代码段除了堆栈的方式,还有clang、查看汇编的方式。

@synchronized (self) {
    i += 1;
}

2.1 查看堆栈


事实证明在这个问题上是不适用的;

2.2 汇编方式

  • 可以看到使用了@synchronize之后在方法块前后调用了两个方法objc_sync_enterobjc_sync_exit;

继续增加objc_sync_enter的符号断点之后;

  • @synchronize是属于libobjc.A.dylib库的;
  • objc_sync_enter在底层callq(调用)函数id2data(objc_object*, usage);

2.3 clang方式

使用命令 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o vc.cpp

  • 根据clang获取编译后的代码,也可以看到熟悉的两个方法objc_sync_enterobjc_sync_exit,同时也验证了汇编方式的结论;

3、objc_sync_enter 源码分析

通过符号断点,得知@synchronize是在我们熟悉的libobjc库中,在我之前的文章中可以得到OC底层探索02- objc4-781 源码编译

enum usage { ACQUIRE, RELEASE, CHECK };

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        // ACQUIRE 枚举值
        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;
}

BREAKPOINT_FUNCTION(
    //其实什么都没有做
    void objc_sync_nil(void)
);

看到这段代码之后再回头看看objc_sync_enter的汇编部分,是不是发现其实汇编也就那样;

  • data->mutex.lock()这才是真正的加锁操作,是系统recursive_mutex_t递归互斥锁的更高层封装;
  • 如果传入的obj是个空值,系统是没有做任何事的,所以在使用时要保证标示对象一定不能为空
  • 通过异常判断之后进入函数id2data(obj, ACQUIRE);

4、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(obj, RELEASE)只是第二个参数不一样。提现了无处不在的抽象和封装思想;

5、id2data(obj, enum usage) 核心函数

代码非常长,这里分为四步分来分析

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;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        // 第一部分
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        // 第二部分
    }

    lockp->lock();


    //第三部分
    
 done:
    lockp->unlock();
    if (result) {
        // 第四部分
    }

    return result;
}

3.3.1 第一部分 快速缓存

#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;
        // lockCount标记该锁的加锁次数
        result = data;
        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: {
            lockCount++;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
            break;
        }
        case RELEASE:
            lockCount--;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
            if (lockCount == 0) {
                // remove from fast cache
                // 缓存次数为0后,将快速缓存对象制空
                tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                // atomic because may collide with concurrent ACQUIRE
                // 原子性的对缓存对象的线程使用数减一
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        }
        //找到处理完lockCount后直接返回
        return result;
    }
}
#endif
  • 在没有特别设置:SUPPORT_DIRECT_THREAD_KEYS默认为1;
  • 当前缓存的快速缓存: 当前线程第一次加锁的对象会被定义为快速缓存;(大多数情况下,一条线程只会使用一个标示对象进行加锁);
  • SYNC_DATA_DIRECT_KEYSYNC_COUNT_DIRECT_KEY都是在当前线程的局部缓存中查找缓存对象SyncData缓存次数lockCount
3.3.1 SyncData

在快速缓存阶段,系统保存了结构为SyncData的对象。

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // 使用该锁的线程数
    recursive_mutex_t mutex;    // 递归互斥锁
} SyncData;
  • SyncData锁对象对象,是一个链表结构;
  • SyncData将synchronize锁所需要的数据进行保存;

3.3.2 第二部分 慢速缓存

这一部分涉及到了慢速缓存,如果在快速缓存中没有找到则会来到这部分;

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

        // 这部分和快速缓存操作基本一致
        result = item->data;
        if (result->threadCount <= 0  ||  item->lockCount <= 0) {
            _objc_fatal("id2data cache is buggy");
        }
        switch(why) {
        case ACQUIRE:
            item->lockCount++;
            break;
        case RELEASE:
            item->lockCount--;
            if (item->lockCount == 0) {
                // 缓存数组的总个数减少
                cache->list[i] = cache->list[--cache->used];
                // 原子性操作
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        }
        return result;
    }
}
  • 测试后发现,慢速缓存也是从当前线程的进行查找
  • cache->list[i] = cache->list[--cache->used];将数组最后一个对象移动到当前下标位置,然后将数组进行缩容;
  • 通过这个双重缓存结构,提高了锁对象syncdata的查找效率;
3.3.2 SyncCache

在慢速缓存中出现了这样一个结构SyncCache.

typedef struct {
    SyncData *data; // 锁对象
    unsigned int lockCount;  // 缓存次数
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;  // 缓存数组的个数
    SyncCacheItem list[0];  // 锁对象的列表
} SyncCache;
  • SyncCache是慢速缓存的实体体现;
  • SyncCacheItem包含了SyncData锁对象以及该锁对象的缓存次数;

3.3.3 第三部分

在双重缓存下都没有命中后会来到这部分,这部分会在:初次加锁同一对象不同线程加锁的时候进入.

lockp->lock();
    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //  listp在函数最开始进行的获取
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                // 同一对象不同线程加锁进入这里
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            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;
        }
    }
    // 全新创建
    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;
  1. 同一对象不同线程加锁时会进行原子的threadCount++
  2. 由于list中SyncData不会进行删除,所以需要复用
  3. 如果1、2步都没有名字,则进行全新创建,并保存到节点的第一个;
3.3.3 StripedMap

listp是在函数最开始进行获取,锁对象存储结构。通过对object的地址hash计算后确定数组下标;

SyncData **listp = &LIST_FOR_OBJ(object);

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    //哈希算法
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
}
  • StripedMap在OC底层探索19-weak和assign区别浅谈在分析weak存储结构时也出现过,都是通过hash算法来进行分组,减少数据查找的难度;
  • 不同的是weak的StripedMap对应的是一张SideTable表;而@synchronized的StripedMap对应的是一个链表结构;
    synchronized结构

3.3.4 第四部分 done

done:
    lockp->unlock();
    if (result) {
        // 解锁流程
        if (why == RELEASE) {
            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
        // 否则保存到当前线程的慢速缓存list中
        {
            // 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;
  • 在第三部分处理完之后都会来到done中;
  • 快速缓存和慢速缓存会互斥存在

总结

通过函数id2data的参数完成了加、解锁操作。并且使用了快速缓存、慢速缓存双重缓存,来提高synvData的命中速度。除此之外stiped+syncData链表对锁实体进行保存。利用threadCount+lockCount实现了多线程、重复加、解锁操作;
通过这些操作提高了递归锁的安全性,但是也降低了性能;

补充

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

还有的几种锁,以后有机会在探索吧~毕竟大部分都在Founation库中,不是很好分析。

欢迎在留言区和我沟通!

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

推荐阅读更多精彩内容