@synchronized底层探索&其他锁

锁的性能排行

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

推荐阅读更多精彩内容