33、iOS底层分析 - 线程锁(二) @synchronized

大纲:

 1、@synchronized 分析

2、@synchronized 注意点

3、@synchronized 坑点

一、@synchronized分析

@property(nonatomic, assign) NSUInteger ticketCount;


- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketCount = 20;
    [self lock_testDemo1];
}
//1、@synchronized
/*
 模拟抢票场景:
 总共有20张票,多个线程同时运行进行抢票。
 */
-(void)lock_testDemo1
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i=0; i<5; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i=0; i<6; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i=0; i<4; i++) {
            [self saleTicket];
        }
    });
    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-04-06 20:41:56.015299+0800 filedome[1160:24672] 当前余票还剩:18
2020-04-06 20:41:56.015552+0800 filedome[1160:24250] 当前余票还剩:18
2020-04-06 20:41:56.015607+0800 filedome[1160:24689] 当前余票还剩:17
2020-04-06 20:41:56.015628+0800 filedome[1160:24678] 当前余票还剩:16
2020-04-06 20:41:56.015827+0800 filedome[1160:24250] 当前余票还剩:15
2020-04-06 20:41:56.015864+0800 filedome[1160:24672] 当前余票还剩:14
2020-04-06 20:41:56.015991+0800 filedome[1160:24678] 当前余票还剩:12
2020-04-06 20:41:56.015997+0800 filedome[1160:24689] 当前余票还剩:12

//*********** 打开注释加锁 @synchronized (self)  ******************
2020-04-06 20:44:50.107009+0800 filedome[1257:26138] 当前余票还剩:19
2020-04-06 20:44:50.107190+0800 filedome[1257:26138] 当前余票还剩:18
2020-04-06 20:44:50.107326+0800 filedome[1257:26138] 当前余票还剩:17
2020-04-06 20:44:50.107438+0800 filedome[1257:26138] 当前余票还剩:16
2020-04-06 20:44:50.107540+0800 filedome[1257:26138] 当前余票还剩:15
2020-04-06 20:44:50.107649+0800 filedome[1257:26137] 当前余票还剩:14
2020-04-06 20:44:50.107739+0800 filedome[1257:26137] 当前余票还剩:13
2020-04-06 20:44:50.108030+0800 filedome[1257:26137] 当前余票还剩:12

通过上面的会发现,不加锁的时候第18、12张票造成了资源抢夺现象,这种情况就叫做线程不安全,当加锁以后就买票顺序就很正常了。

分析为什么 @synchronized 可以解决多线程同时访问的问题,底层是怎么做的呢?
首先想到的分析方法由汇编、clang、找源码。

从汇编入手,先看一下底层都调用了什么方法,然后去找源码查看一下具体实现。

//  main.m
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
    
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        @synchronized (appDelegateClassName) {
        }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

在@synchronized (appDelegateClassName)  打断点汇编调试

用clang 分析一下main.m 中的 @synchronized
 终端 cd 到对应main.m 文件夹,然后

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

为什么需要这么一长串呢? 因为main.m  #import <UIKit/UIKit.h> 环境变量等一系列无法访问到所以要加上 isysroot 地址。
 
 然后查看main.cpp,翻到最后面看。调整过排版之后的代码如下

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    / * @autoreleasepool * / { __AtAutoreleasePool __autoreleasepool;
        
        
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        {
            id _rethrow = 0;
            id _sync_obj = (id)appDelegateClassName;
            objc_sync_enter(_sync_obj);
            try {
                struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
                    ~_SYNC_EXIT() {
                        objc_sync_exit(sync_exit);
                    }
                    id sync_exit;
                } _sync_exit(_sync_obj);
                ......

对照原代码,@synchronized 是如下两行代码包裹实现的
 objc_sync_enter();
 objc_sync_exit();

即便是看到了这两行代码,但是这个是在哪里实现的,怎么实现的呢?
既然知道了加锁代码是在 objc_sync_enterobjc_sync_exit之间执行的,可以查看一下这两个函数的具体实现,通过打符号断点查看一下这两个函数实现在哪里。

通过汇编调试(左边堆栈信息和跳转进去后的头部信息)我们发现objc_sync_enter 其实是在libobjc.dylib 库里面,也就是objc的源码,我们打开objc的源码搜索一下objc_sync_enter,找到它的具体实现。
源码 目录:demos->003-锁分析->1-objc4-756.2源码
根据源码注释我们知道@synchronized是基于obj的递归锁。
递归锁   递归锁是一种特殊的互斥锁

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(); 点进去查看 objc_sync_nil();

 BREAKPOINT_FUNCTION(
     void objc_sync_nil(void)
 );

发现什么都没有做。由此可以知道。如果说我们的
 @synchronized (self) {}  self 被释放的话,什么都不做(也就是没有加锁成功)。这时候调其包裹住的代码的话仍然会出现问题。结合内存管理的生命周期的话,@synchronized (self) {}递归锁的生命周期同传入的self。


也就是说当@synchronized(nil){}时候,其实是没有加锁的,所以在使用 @synchronized(obj) 时候,我们要注意保证obj 没有被释放才能加锁成功
 
再看一下结构体 SyncData

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;          //类似子类,找下层。构成一个链式结构,这是一个结点
    DisguisedPtr<objc_object> object;   //锁住的对象
    int32_t threadCount;                //使用此块的线程数
    recursive_mutex_t mutex;            //递归锁
} SyncData;

递归锁 + objc_sync_nil(objc 置为 nil) 搭配使用的时候可以 防止死锁
 查看一下 id2data();
 @synchronized - 在vc model view 等任何地方都可以直接使用,感觉应该是一个全局的,

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) {//
            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);
                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);
                        break;
                    }
                    case RELEASE://解锁,如果解锁,则对加锁次数-1,然后重新缓存到线程的单项快速缓存中
                        lockCount--;
                        tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                        if (lockCount == 0) {//lockCount为0时候就从tls中删除解锁
                            // remove from fast cache
                            tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                            // atomic because may collide with concurrent ACQUIRE--记录的线程数去掉,threadCount是记录的线程的情况
                            OSAtomicDecrement32Barrier(&result->threadCount);
                        }
                        break;
                    case CHECK:
                        // do nothing
                        break;
                }
                
                return result;
            }
        }
#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;
                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) {
                            // 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();
        
        {
            /*
             再次从缓存中取,看看有没有,避免有其他线程已经对obj加锁
             如果找到了就对threadCount+1
             如果找不到就在SyncData接一个data
             
             如果不是加锁咋不管
             如果是加锁,则将新的data的objc设置为object,并且将threadCount加1
             
             如果一个SyncData都没有那么就创建一个,设置好数据后保存在map中
             最后如果是新创建的,那么就会在tls中保存数据
             */
            SyncData* p;
            SyncData* firstUnused = NULL;
            for (p = *listp; p != NULL; p = p->nextData) {
                if ( p->object == object ) {//再次从缓存中取一下
                    result = p;
                    // atomic because may collide with concurrent RELEASE
                    //线程+1
                    OSAtomicIncrement32Barrier(&result->threadCount);
                    goto done;
                }
                //如果已经有了SyncData但是没有对应的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 ) {//则将object配置在后面的SyncData中
                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都没有,那就创建一个
        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) {//如果不是从缓存中读取的,那么就将SyncData缓存到tls中
                // 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;
    }

查看一下 LOCK_FOR_OBJ() 这个宏

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

每个加锁的obj 对应一个SyncList,再看一下 StripedMap

//泛型写法,真正的类型是 SyncList。
 static StripedMap<SyncList> sDataLists;

 struct SyncList {
     SyncData *data;
     spinlock_t lock;

     constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
 };

再查看 StripedMap

class StripedMap {
    ......
        static unsigned int indexForPointer(const void *p) {
            uintptr_t addr = reinterpret_cast<uintptr_t>(p);
            return ((addr >> 4) ^ (addr >> 9)) % StripeCount; // 哈希函数
    ......

可以发现StripedMap 其实是一个抽象类,并且里面有一个 indexForPointer 的存储函数。
 这个存储函数是通过哈希函数得到一个index 。而且 @synchronized(obj) 可以全局使用
 结合其结构可以猜测 stripedMap 应该是一个全局的哈希表来通过 obj 得到一个index,存储在SyncList里面,而SyncList 存储的都是一个个的结点 SyncData
 也就是通过拉链法 哈希结构来存储的,结构如下图
 SyncData 就是我们加锁的对象,obj 进来之后会被封装成相应的SyncData 然后存在整个的SyncList 里面。
 
 
 id2data中

检查每个线程单项快速缓存中是否有匹配的对象
检查已拥有锁的每个线程高速缓存中是否有匹配的对象
如果没有找到,就去创建一个链表结构,然后去相应的下标里面偏移进行存储
也就是说锁过的data 在用的时候直接去取就行了
 
 @synchronized 是一种SynList 表结构,存储的是一个个结点(SynData(封装的obj))
 在底层会事项一些相应的线程操作 TLS(线程的局部存储空间)
 @synchronized 是对互斥锁的一种封装,对象封装递归形式的。里面是表结构过,通过哈希来存储。
 互斥锁,用于保证任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入休眠,等待锁释放时被唤醒。
 互斥锁又分为递归锁和非递归锁
 

@synchronized
 小结:
 步骤1、当加锁的时候,系统会先找到当前线程的tls(线程局部存储) 中查找obj 对应的SyncData。
    (1)、如果找到了执行 步骤2。
    (2)、如果没有找到就从全局的SyncCache 中遍历寻找 obj 对应的SyncCacheItem。
        ①、如果取到了就到 步骤2。
        ②、如果没有取到就到 步骤3。


 步骤2、找到并取出SyncData 或者 SyncCachetItem,先取出对象的 lockCount,代表的是加锁的次数。
    (1)、如果是加锁,则把lockCount +1 ,保存到线程的 tls 中
    (2)、如果是解锁,则把lockCount -1 ,保存到线程的 tls 中。如果减完之后的 lockCount 为 0 则将tls 中lockCount 置空(nil),并且通过 OSAtomicDecrement32Barrier 将 SyncData 的线程数置空,然后将SyncData 或者SyncCacheItem 返回


 步骤3、假如没找到,说明该对象以前没加锁过 或者是 在其他的SyncData中,为了保证不重复创建SyncData,进行如下操作。
    (1)、先从listp 中遍历SyncData(源码中有注释,以前的做法是在多个data里对同一个对象加锁,所以找到data 发下没有该对象的话继续去遍历因为有可能在其他的data中。所以不应该单纯的只查一个data 而是去查整个链表。这个遍历过程加了锁,更安全),如果找到 obj 对应的SyncData(可能其他线程创建的),执行 步骤4
    (2)、如果没有找到 obj 对应的SyncData,但是有空白的 SyncData,则将空白的SyncData 与obj 关联起来,并且将 SyncData 的threadCount (线程数)置为1,再执行 步骤4
    (3)、假如没有空白的SyncData,则创建一个SyncData,然后与obj 关联起来将SyncData 的 threadCount 置为1,并且将创建的SyncData 变为listp 的第一个元素(listp 是通过obj 哈希算法得到index 存储在map 表中,因为不同obj 可能得到相同的index,所以此时listp 是已经有数据了,为了不让数据丢失,会吧数据赋值在取到数据的nextData 中,类似栈特征,先进的在后面)


 步骤4、判断是否是release 也就是是解锁那么直接返回nil(因为在这个当前线程没有找到,在其他线程中找到的。例如线程1的tls 中没有找到,在线程2中找到的)  准备好了SyncData,则解锁上面代码,如果是解锁,则不管直接return,如果是加锁,则将准备好的SyncData 保存在当前线程tls的SYNC_DATA_DIRECT_KEY 中,并将tls中 SYNC_DATA_DIRECT_KEY 设置为1,然后将SyncData 返回。

 记录lockCount 进行递归调用, id2data( why)是根据加锁还是解锁存入不同给的
 lockCount 可以重入,可以被锁多次,所以@synchronized 是一个递归锁(可以被锁多次)。
 因为@synchronized 记录了我们的线程和锁的情况,就能知道哪些线程造成了相互等待,这个时候只要通过处理这个相互等待就不会死锁(例如发送一个强大的命令什么的)
 由于@synchronized需要不断对map 表已经缓存进行读写操作,所以性能比较低
 


 @synchronized 坑点。

当比较多的线程去创建 _testArray 的时候,其实 _testArray 的初始化这个时候会去创建新值,释放旧值(new value   old values release)。旧值保存在一个临时变量去释放,如果说大量的线程同时去访问,就会存在多个同时去释放的情况,就会崩溃。这时候需要加锁。
 @synchronized (_testArray) 这样写的话还是不行,看上去是加锁锁住了 _testArray,但是上面分析源码的时候我们知道,当传进去的obj 为nil的时候什么都不做。也就是说数组 _testArray 传进去的时候这时候要old release,release 瞬间这个old 就变成了nil。在@synchronized 底层是通过哈希结构去找这个对象是否已存在,找到直接用。但是找的的这个已经变成了nil 相当于对nil 加锁没有任何作用。

    for (int i=0; i<200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
//            @synchronized (_testArray) {//会崩溃
            @synchronized (self) {//不会崩溃
                _testArray = [NSMutableArray array];
            }
        });
    }

修改

在不断循环递归,多线程操作的时候,同时又有置换的时候 @synchronized 有时候并不能达到我们想要的目的,同时在底层不断的进行增删改查的一些操作,在性能方面是有些影响的。好做是非常简单一句代码就可以搞定。所以在有些地方可以换成其他锁来解决。
例如:NSLock

21、iOS底层分析 - 线程锁(一)NSLock

    NSLock * lock = [[NSLock alloc] init];
    for (int i=0; i<200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            _testArray = [NSMutableArray array];
            [lock unlock];
        });
    }

递归锁最容易犯的错误就是死锁。
例如 如下代码就会死锁,因为在多个线程调用的时候每次进来就会加锁,线程与线程之间互相等待(线程1锁了还没解锁线程2又锁了,线程1要解锁需要等线程2解锁,线程2解锁需要等线程1解锁),就造成了死锁。
 例如线程2 进来加锁,还没到解锁,线程3又进来加了锁,这时候线程2要想解锁必须等待线程3解锁,但是线程3要解锁又必须等待线程2解锁,所以线程与线程间等待。在同一个线程内是递归调用,递归锁能控制。但是线程与线程间不行。
 但是换成 @synchronized就不会。原因前面说了,是因为它加锁之后再次进来回去缓存中找如果有了就不再添加新的锁,所以不会造成一直添加锁的锁等待。

    NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
    for (int i=0; i<100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void(^testMethod)(int);
            testMethod = ^(int value){
                //@synchronized (self) {//解决错误
                [lock lock];
                if (value >0) {
                    NSLog(@"current value = %d",value);
                    testMethod(value-1);
                }
                [lock unlock];
                //}
            };
            testMethod(10);
        });
    }

打印一下错误原因

 

通过控制台打印出来的错误原因分析,然后修改使用 @synchronized 替换 NSLock。

互斥锁使用:
 普通线程安全 可以使用                                       NSLock                        非递归(对象)锁
 递归调用的 用                                                      NSRecursiveLock       递归锁
 循环又收到外界线程影响的时候更多关注死锁   @synchronized           递归锁

   

总结

  1. @synchronized()是递归锁,同一线程可重入,只是内部有个持锁计数器而已
  2. 进入@synchronized()代码块时会执行objc_sync_enter(id obj)
  3. 退出@synchronized()代码块时会执行objc_sync_enter(id obj)
  4. 核心方法是通过id2data()来获取到对象锁SyncData
  • 首先从线程的私有数据(快速缓存)中查找
  • 再从全局缓存中查找。全局缓存是一张Hash表,通过对obj指针经过Hash计算出索引,Hash表value是链表第一个节点的地址,链表中每个节点对应不同对象的锁SyncData
  • 如果没有缓存,则从全局缓存的链表中找到第一个空闲节点,直接使用该节点,将其跟obj关联起来
  • 如果没有空闲节点,开辟空间创建新节点,并插入链表的头部
  • 将结果缓存到线程的私有数据中,并保存在全局缓存中。
发布了104 篇原创文章 · 获赞 13 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/105352579