iOS八股文(十四)iOS中的锁

1,253 阅读6分钟

线程安全

线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 在iOS中,UIKit是绝对线程安全的,因为UIKit都是在主线程操作的,单线程没有线程当然没有线程安全问题,但除此之外,其他都要考虑线程安全问题。

iOS解决线程安全的途径其原理大同小异,都是通过锁来使关键代码保证同步执行,从而确保线程安全性,这一点和多线程的异步执行任务是不冲突的。类似骑自行车去酒吧,该省省,该花花。

经典案例

经典的买票问题,假设德云色的相声演出,有50张门票,为方便大家买票,在北京分了5个点售票,一直到售完为止。

#pragma mark - 买票问题,
//卖票演示
- (void)ticketTest{
    self.ticketsCount = 50;
    self.sellCount = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    for (NSInteger i = 0; i < 5; i++) {
        dispatch_async(queue, ^{
            while(self.ticketsCount > 0) {
                [self sellingTickets];
            }
        });
    }
    //过了2s,老板来查账
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
        NSLog(@"卖出的票数-> %lu", (unsigned long)self.sellCount);
    });

}
//卖票
- (void)sellingTickets{
    NSUInteger oldCount = self.ticketsCount;
    //模拟耗时操作
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    //都在一条线程,确保安全性
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    
}

执行结果如下:

image.png 可以看到,最后统计出来居然是63,显然这样直接的去操作是线程不安全的。

接下来就用iOS中的各式各样的锁🔒来解决这个问题。下面个别锁的初始化:

- (void)initLock {
    _spinLock = OS_SPINLOCK_INIT;
    
    _unfairLock = OS_UNFAIR_LOCK_INIT;
    
    _nsLock = [[NSLock alloc] init];
    
    _nsCondition = [[NSCondition alloc] init];
    
    _nsConditionLock = [[NSConditionLock alloc] init];
  
     _semaphore = dispatch_semaphore_create(1);
}

OSSpinLock

OSSpinLock是在libkern库中,使用之前需要引入头文件<libkern/OSAtomic.h>,使用时会出现警告⚠️

image.png 这是因为OSSpinLock存在缺陷,从iOS10开始已经不建议使用了。官方建议使用os_unfair_lock来替代。

#pragma mark -spinlock
/**
 OSSpinLock 自旋锁,隶属于 #import <libkern/OSAtomic.h>
 有bug,优先级反转
 iOS10 就已经弃用
 */
- (void)sellingTicketsBySpinLock {
    //_spinLock已经提前初始化
    //_spinLock = OS_SPINLOCK_INIT; 
    OSSpinLockLock(&_spinLock);
    NSUInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        OSSpinLockUnlock(&_spinLock);
        return;
    }
    sleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    OSSpinLockUnlock(&_spinLock);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
}

执行打印结果:

image.png

os_unfair_lock

os_unfair_lock 在os库中,使用之前需要导入头文件<os/lock.h>

image.png

- (void)sellingTicketsByUnfairLock {
    os_unfair_lock_lock(&_unfairLock);
    NSInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        os_unfair_lock_unlock(&_unfairLock);
    }
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    os_unfair_lock_unlock(&_unfairLock);
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    }); 
}

执行后结果:

image.png

可以看到非常的安全🔐,关于这个不公平锁的分类,网上产生了很多分歧,这里查阅官方文档并翻译:

image.png

可以看到这里的解释是,不是旋转(忙等),而是休眠,等待被唤醒,所以个人认为os_unfair_lock互斥锁

NSLock

当然我们Foundation框架内部也是有一把🔒-NSLock,使用起来非常方便,基于pthroad_mutex封装而来,是一把互斥非递归锁

- (void)sellingTicketsByNSLock {
    [_nsLock lock];
    NSUInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        [_nsLock unlock];
        return;
    }
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    [_nsLock unlock];
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
}

同样可以解决问题。

可以通过swift foundation 源码来看一看NSLock的实现,从而更深层次的了解NSLock

直接找到NSLock.swift文件。

image.png

  • 构造方法init()就是调用了pthread的pthread_mutex_init(mutex, nil)方法
  • 析构方法deinit就是调用了pthread的pthread_mutex_destroy(mutex)方法
  • 加锁方法 lock()就是调用了pthread的pthread_mutex_lock(mutex)方法
  • 解锁方法 unlock()就是调用了pthread的pthread_mutex_unlock(mutex)方法 其根本是对pthread_mutex的封装,在pthread_mutex中可以通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))来设置锁为递归锁,这里并没有设置,所以在使用的时候需要注意NSLock并不能在递归函数中使用。

NSCondition

#pragma mark - NSCondition
- (void)sellingTicketsByNSCondition {
    [_nsCondition lock];
    NSUInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        [_nsCondition unlock];
        return;
    }
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    [_nsCondition unlock];
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
}

执行结果:

image.png 同样在NSLock.swift文件可以找到NSCondition的实现:

image.png 可以看到一样是基于pthroad_mutex封装。

相比于NSLockNSCondition多了几个API:

image.png

  • (void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。
  • (void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。
  • (void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
  • (void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。

注意⚠️:pthread_mutex 存在虚假唤醒的情况,一个signl唤醒多个wait。 在编码过程中可以通过while条件判断,使被唤醒的线程,陷入while循环中,从而解决此问题。一般在生产者问题中出现。

NSConditionLock

#pragma mark - NSConditionLock
- (void)sellingTicketsByNSConditionLock {
    [_nsConditionLock lock];
    NSUInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        [_nsConditionLock unlock];
        return;
    }
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    [_nsConditionLock unlock];
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
}

执行结果:

image.pngNSLock.swift文件可以找到NSConditionLock的实现:

image.png 这里是对NSCondition二次封装。

其实NSConditionLock的功能不止于此,NSConditionLockinitlockunlock中都可以传入value。例如:

- (void)test {
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:3];
    dispatch_queue_t globalQ = dispatch_get_global_queue(0, 0);
    dispatch_async(globalQ, ^{
        [self.conditionLock lockWhenCondition:3];
        NSLog(@"任务1");
        [self.conditionLock unlockWithCondition:2];
    });
    
    dispatch_async(globalQ, ^{
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"任务2");
        [self.conditionLock unlockWithCondition:1];
    });
    
    dispatch_async(globalQ, ^{
        [self.conditionLock lockWhenCondition:1];
        NSLog(@"任务3");
        [self.conditionLock unlockWithCondition:0];
    });
}

如此执行,可保证任务执行顺序总是1⃣️2⃣️3⃣️:

image.png

NSRecusiveLock

英语小课堂: recursive -- 递归

#pragma mark  NSRecursiveLock
- (void)sellingTicketsByNSRecursiveLock {
    [_recursiveLock lock];
    NSUInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        [_recursiveLock unlock];
        return;
    }
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    [_recursiveLock unlock];
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
}

执行结果: image.png 可以看到,这里并没有起到作用。我们先在源码中看其实现:

image.png 通过PTHREAD_MUTEX_RECURSIVE来设置锁为递归锁。当锁为递归锁的时候,它的使用场景为单个线程中的递归调用,显然我们这里的需求并不适合,所以会出问题。

@synchronized

@synchronized也是可以保证代码同步:

#pragma mark -@synchronized
- (void)sellingTicketsBySynchronized {
    @synchronized (self) {
        NSUInteger oldCount = self.ticketsCount;
        if (oldCount == 0) {
            return;
        }
        usleep(arc4random_uniform(30));
        oldCount -= 1;
        self.ticketsCount = oldCount;
        NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.sellCount ++;
        });
    }
}

结果也是线程安全:

image.png

Semaphore信号量

同样的信号量也可以解决线程安全问题:

#pragma mark - samaphore
- (void)sellingTicketsBySemaphore {
    //信号量的初始化
    //_semaphore = dispatch_semaphore_create(1);
    dispatch_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSInteger oldCount = self.ticketsCount;
    if (oldCount == 0) {
        dispatch_semaphore_signal(self.semaphore);
        return;
    }
    usleep(arc4random_uniform(30));
    oldCount -= 1;
    self.ticketsCount = oldCount;
    dispatch_semaphore_signal(self.semaphore);
    NSLog(@"当前剩余票数-> %lu", (unsigned long)oldCount);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.sellCount ++;
    });
}

image.png

关于信号量的使用和原理,在之前的文章中有提及。

锁的分类

锁按照等待师的状态可以分为互斥锁和自旋锁。

  • 互斥锁在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时线程会被唤醒,例如spinlock
  • 自旋锁的线程则会一直处于等待状态(忙等待)不会进入休眠,NSLockNSConditionNSConditionLockNSRecursievLock

锁的运行效率

锁在运行的时候的效率也各不相同,以下是对比。其中@synchronized在真机条件下效率会有所提升。在开发时,我们可以根据不同情况,选择适合的锁,来保证我们的代码线程安全。 image.png

参考链接