八大锁分析

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 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 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:"方法。

你可能感兴趣的:(八大锁分析)