美文网首页
八大锁分析

八大锁分析

作者: 半边枫叶 | 来源:发表于2020-03-15 16:34 被阅读0次

    synchronized分析

    我们先来看个题目:

    - (void)lg_testSaleTicket{
        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 < 5; i++) {
                [self saleTicket];
            }
        });
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 10; i++) {
                [self saleTicket];
            }
        });
    }
    
    - (void)saleTicket{
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张",self.ticketCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
    }
    

    然后我们调用上面的方法

    self.ticketCount = 20;
    [self lg_testSaleTicket];
    

    请问上面的代码设计是否有问题呢?
    当然有问题,会存在多个线程操作一个数据ticketCount,导致数据不安全的问题。执行完成后剩余的票数可能不会为0。
    既然是多线程导致的数据不安全问题,我们就可以加锁进行解决。

    - (void)saleTicket{
        // 枷锁 - 线程安全
        @synchronized (self) {
            if (self.ticketCount > 0) {
                self.ticketCount--;
                sleep(0.1);
                NSLog(@"当前余票还剩:%ld张",self.ticketCount);
            }else{
                NSLog(@"当前车票已售罄");
            }
        }
    }
    

    我们对卖票的操作部分加上了@synchronized,这样同时只能有一个线程操作ticketCount,从而保证了数据的安全。
    下面我们来探究下@synchronized。

    appDelegateClassName = NSStringFromClass([AppDelegate class]);
    @synchronized (appDelegateClassName) {
    }
    

    在synchronized的地方打上断点,然后汇编调试。


    image.png

    在synchronized的汇编调试代码中,我们看有objc_sync_enter和objc_sync_exit成对的出现。所以这一对函数应该是和synchronized的底层实现相关的。
    然后我们就可以通过符号断点,针对objc_sync_enter打个符号断点


    objc_sync_enter符号断点
    这样我们可以看到objc_sync_enter位于libobjc.A.dylib动态库中,然后我们就可以去open.souce上下载这个源码了。

    下载objc源码,然后搜索objc_sync_enter

    // Begin synchronizing on 'obj'. 
    // Allocates recursive mutex associated with 'obj' if needed.
    // Returns OBJC_SYNC_SUCCESS once lock is acquired.  
    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;
    }
    

    synchronizing是种互斥锁。首先判断objc,如果不存在的话走objc_sync_nil(),也就是什么都不做。所以在使用@synchronized(obj)进行加锁的时候,如果obj为nil,就是无效的,不会进行加锁。

    下面我们看下objc不为空的情况:
    构建了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;
    

    里面有个nextData,应该是指向了下一个节点。所以好多这样的节点组成了一个链表似的结构;里面还有个递归锁mutex(递归锁属于互斥锁的一种)。

    static SyncData* id2data(id object, enum usage why)
    {
        spinlock_t *lockp = &LOCK_FOR_OBJ(object);
        // ....... 
    }
    

    在id2data函数中通过LOCK_FOR_OBJ函数获取到lockp,LOCK_FOR_OBJ函数的定义如下

    #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
    static StripedMap<SyncList> sDataLists;
    

    可以看到sDataLists实际上是一个哈希表,表中存在一个个的SyncList对象,SyncList对象的结构中有data和lock。

    struct SyncList {
        SyncData *data;
        spinlock_t lock;
    
        constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
    };
    

    @synchronized底层是封装的互斥锁pThread。

    synchronized使用注意点

    下面代码可以正常运行吗?

    - (void)lg_crash{
        for (int i = 0; i < 200000; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                _testArray = [NSMutableArray array];
            });
        }
    }
    

    执行这段代码会导致野指针crash。
    GCD里面的_testArray = [NSMutableArray array];这句代码是在创建新的Array赋值给_testArray,然后释放了旧值。如果此时多个线程同时暂存了旧值,然后就会导致多次释放同一个旧值,从而产生野指针崩溃。
    我们可以进行加锁处理。像下面的这样加锁处理可以吗?

    - (void)lg_crash{
        for (int i = 0; i < 200000; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                @synchronized (_testArray) {
                    _testArray = [NSMutableArray array];
                }
            });
        }
    }
    

    答案是会产生同样的野指针crash。因为在过程中_testArray可能为空,使用@synchronized锁的对象如果为空的话,相当于不锁。所以会得到同样的crash。此时我们可以将锁的对象_testArray换成self,这样就可以解决问题。但是@synchronized底层需要对哈希表进行处理,过程比较复杂,所以效率低。这里我们可以使用NSLock来进行加锁处理。

    - (void)lg_crash{
        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];
            });
        }
    }
    

    NSLock分析

    下面的代码可以正常执行吗?

    NSLock *lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
            }
            [lock unlock];
        };
        testMethod(10);
    })
    

    答案是只打印出一个10,就会卡死。
    因为递归调用了testMethod,就会多次进行lock加锁,在一个lock锁定的区域内递归调用再次进行加锁,就会导致堵塞。
    因为是递归调用,此时我们应该讲NSLock换成递归锁NSRecursiveLock,就能正常的打印出10 9 8 7 6 5 4 3 2 1 了。

    我们在上面代码的最外层再加一个for循环,还可以正常执行吗?

    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){
                [lock lock];
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [lock unlock];
            };
            testMethod(10);
        });
    }
    

    这样就会导致死锁的问题。多个线程进行加锁,互相等待,导致死锁。此时我们只需要将递归锁换成@synchronized就可以解决死锁问题了。因为@synchronized的底层的实现,如果已经锁过一次了就会从缓存中取,而不会再次加锁了。
    总结:普通的线程安全可以使用NSLock;如果存在递归调用,使用NSRecursiveLock;如果内部存在递归,外部存在循环或者有其他线程影响,使用@synchronized。

    条件锁:NSCondition

    调用下面的lg_testConditon方法,会有问题吗?

    - (void)lg_testConditon{
        //创建生产-消费者
        for (int i = 0; i < 50; i++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self lg_producer];
            });
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                [self lg_consumer];
            });
        }
    }
    
    - (void)lg_producer{
        self.ticketCount = self.ticketCount + 1;
        NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    }
    
    - (void)lg_consumer{
        while (self.ticketCount == 0) {
            NSLog(@"等待 count %zd",self.ticketCount);
        }
        
        //注意消费行为,要在等待条件判断之后
        self.ticketCount -= 1;
        NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    }
    

    上面的代码因为在多线程中,不能保证数据安全。我们需要加锁处理?这里NSCondition就最合适了。使用NSCondition当消费到ticketCount为0的时候,调用wait等待。当生产一个ticket后,调用signal发送信号,让等待的可以继续执行。代码实现如下:

    
    - (void)lg_producer{
        [_testCondition lock];
        self.ticketCount = self.ticketCount + 1;
        NSLog(@"生产一个 现有 count %zd",self.ticketCount);
        [_testCondition signal];
        [_testCondition unlock];
    }
    
    - (void)lg_consumer{
        // 线程安全
        [_testCondition lock];
        while (self.ticketCount == 0) {
            NSLog(@"等待 count %zd",self.ticketCount);
            // 保证正常流程
            [_testCondition wait];
        }
        //注意消费行为,要在等待条件判断之后
        self.ticketCount -= 1;
        NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
        [_testCondition unlock];
    }
    

    首先锁住生产和消费的代码,然后在消费的时候如果发现ticketCount为0,就wait等待。生产后发送signal,让等待的继续执行消费。

    条件锁:NSConditionLock

    // 信号量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
       [conditionLock lockWhenCondition:1];
       NSLog(@"线程 1");
       [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       [conditionLock lockWhenCondition:2];
       NSLog(@"线程 2");  
       [conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
    

    首先创建一个NSConditionLock条件锁,并且设置condition为2。
    [conditionLock lockWhenCondition:1];的意思是如果此时的condition为1,并且没有其他线程获取锁,那么就可以获取锁执行下面的代码。[conditionLock unlockWithCondition:0];的意思是释放锁,并且将条件置为0。[conditionLock lock];的意思是不受condition条件的影响。
    了解了NSConditionLock后,我们可以知道,线程2肯定是在线程1之前执行。

    下面我们来使用汇编来探索一下NSCondition的实现,首先在[conditionLock lockWhenCondition:1];的地方打上断点,然后开启汇编调试

    image.png
    进入到汇编后我们来到objc_msgSend的地方,这里是调用方法的地方,我们通过lldb命令查看x0和x1的值。可以得到x0是NSConditionLock,x1为lockWhenCondition。也就是我们外面的[conditionLock lockWhenCondition:1];这行代码的调用。
    image.png
    我们怎么继续跟踪[conditionLock lockWhenCondition:1];这个方法实现呢?此时我们可以通过符号断点的方式,定位到lockWhenCondition方法的具体执行。添加符号断点-[NSConditionLock lockWhenCondition:]。然后我们点击继续就会断点在lockWhenCondition的实现。在lockWhenCondition的实现汇编代码中又定位到一个objc_msgSend。这里一定是调用了其他的方法。我们打印出方法的执行者和方法名称 image.png

    方法的执行者是NSConditionLock,方法名称为lockWhenCondition:beforeDate:。我们在苹果的官方文档也找到了这个方法。我们继续打符号断点追踪这个方法的实现。

    image.png
    lockWhenCondition:beforeDate:这个方法中定位到一个objc_msgSend。然后打印方法的执行者和方法名,竟然发现是调用了NSCondition的lock方法。也就是说NSConditionLock的底层是通过NSCondition来实现加锁的。
    然后我们继续往下看,发现有个cmp对比x8和x21,如果相等就跳转0x18d5cc040,否则继续往下执行。
    image.png
    打印x8和x21的值,分别为2和1。这个不就是我们在外面使用NSConditionLock设置的条件吗
    image.png
    继续往下走调用了一个"waitUntilDate:"方法。

    相关文章

      网友评论

          本文标题:八大锁分析

          本文链接:https://www.haomeiwen.com/subject/qqfjdhtx.html