iOS多线程之四:八大锁(终结篇)

288 阅读6分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

前言

多线程的学习系列,这是最后一篇了,通过了解八大锁,我们会知道如何去保护线程安全,保护数据的安全。自旋锁,互斥锁,条件锁,递归锁,读写锁等等,这里都会进行一一介绍。这里我先列一下前面写的文章:

锁的性能

锁的大类分为两种:互斥锁自旋锁

互斥锁:互斥锁是用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当前线程得到一个临界区的锁,而此时这个锁正被另外一个线程所持有,那么当前线程就会被阻塞,进入等待队列,处于休眠状态。

自旋锁:线程反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等待,一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

下图是各种锁的性能。

image.png

下面我就按这个顺序来介绍好了。

OSSpinLock

OSSpinLock 是自旋锁,性能是最高的,但是会一直轮询,等待时会消耗大量 CPU 资源。所以苹果已经推荐os_unfair_lock来代替。

上面使用OSSpinLock编译会报警告,在iOS10已经废弃了OSSpinLock大家也就已经不再使用.

os_unfair_lock是一个互斥锁,它不像 OSSpinLock 需要一直忙等。

那就来个售票的例子吧。

@property (nonatomic, assign) os_unfair_lock unfairLock;

先初始化 os_unfair_lock

_unfairLock = OS_UNFAIR_LOCK_INIT;

售票请求:

- (void)jj_test_os_unfair_lock{

    self.ticketCount = 20;

    for (int i = 0; i< 11; i++) {
        dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            [self saleTicket];
        });
    }

    for (int i = 0; i< 11; i++) {
        dispatch_queue_t queue = dispatch_queue_create("jj2.com", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            [self saleTicket];
        });
    }
}

没有加锁的时候

- (void)saleTicket{
    if (self.ticketCount > 0) {
        self.ticketCount--;
        NSLog(@"当前窗口:余票还剩:%lu张",(unsigned long)self.ticketCount);
    }else{
        NSLog(@"当前窗口:当前车票已售罄");
    }
}

我们就看下没有加锁的时候会不会有问题。

20220223115406.jpg

看到没,是不是出问题了。

那如果我们加一下锁呢?

- (void)saleTicket{
    os_unfair_lock_lock(&_unfairLock);
    if (self.ticketCount > 0) {
        self.ticketCount--;
        NSLog(@"当前窗口:余票还剩:%lu张",(unsigned long)self.ticketCount);
    }else{
        NSLog(@"当前窗口:当前车票已售罄");
    }
    os_unfair_lock_unlock(&_unfairLock);
}

来,我们看下效果。

20220223115640.jpg

简单分析一波:我们第一个线程进来的时候先加锁,然后开始对票进行数据处理。第二个线程进来的时候,发现被锁了,就开始休眠等待,等数据减1后了,unlock 了,这时候第二个线程就被唤醒,就进来了,同时加锁处理。这样就能保证数据的安全。这就是互斥锁。

dispatch_semaphore

dispatch_semaphore 用来初始化信号量,通过信号量的值可以控制线程哪个执行,哪个需要等待。并且设置 GCD 的最大并发数。值为 1 的时候,还能达到同步锁的效果。

  • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量。

  • dispatch_semaphore_signal:发送一个信号,让信号总量加 1。

  • dispatch_semaphore_wait:如果信号量大于 0,则正常执行,而且信号量会减 1 ;如果信号量为 0 ,则会一直等待,等接收到通知信号量大于 0 后才可以正常执行。如果等待的时候会起到阻塞当前线程的效果。

我们来个例子看下如果实现同步线程的效果:

- (void)dispatch_semaphore_t_request2
{
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    dispatch_queue_t queue = dispatch_queue_create("jj.com", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{

        NSLog(@"1--%@",[NSThread currentThread]);

        sleep(2);

        NSLog(@"1-完成");

        dispatch_semaphore_signal(sema);

    });

    dispatch_async(queue, ^{

        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

        NSLog(@"2--%@",[NSThread currentThread]);

        sleep(1);

        NSLog(@"2-完成");
    });

    NSLog(@"main--%@",[NSThread currentThread]);

}

20220223122559.jpg

我们可以看到,线程 6 先执行,而且是等任务 1 完成后,线程 7 才开始执行。这就是同步线程锁的作用。异步串行队列的任务都是在一个线程内执行的。这里是不同线程也实现同步线程执行。

pthread_mutex

pthread_mutex 也是一个互斥锁。NSLock 的底层封装就是用 pthread_mutex 。

那我们继续用上面买票的例子。

@property (nonatomic, assign) pthread_mutex_t mutex_t;

初始化锁

pthread_mutex_init(&_mutex_t, NULL);

售票:

- (void)saleTicket{
    pthread_mutex_lock(&_mutex_t);

    if (self.ticketCount > 0) {
        self.ticketCount--;
        NSLog(@"当前窗口:余票还剩:%lu张",(unsigned long)self.ticketCount);
    }else{
        NSLog(@"当前窗口:当前车票已售罄");
    }
    pthread_mutex_unlock(&_mutex_t);
}

结果也是正常的,这就不贴出来了,和上面的os_unfair_lock加锁效果是一样的。

NSLock

NSLock 也是一个互斥锁,底层就是用上面的 pthread_mutex 实现的。

它的使用方法就用 的协议。这里 NSLock 还有其它锁也遵循这个协议。

@protocol NSLocking

- (void)lock;

- (void)unlock;

@end

那这里的例子其实和上面售票是一样的,也是直接用 lock 和 unlock 方法进行加锁处理就行。

NSCondition

NSCondition 是一个条件锁,也是遵循 NSLocking 协议。作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程。

20220223142439.jpg

  • wait:会阻塞线程,使其进入休眠状态,直至超时。
  • waitUntilDate:会阻塞线程,使其进入休眠状态,直至某个 Date 时刻。
  • signal:会唤醒一个正在休眠等待的线程。
  • broadcast:会唤醒所有正在等待的线程。

我们先来一个生产消费者的例子:

- (void)jj_testConditon {

    _testCondition = [[NSCondition alloc] init];

    //创建生产-消费者
    for (int i = 0; i < 20; i++ {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self jj_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self jj_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self jj_consumer];
        });

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self jj_producer];
        });
    }

我们给生产者加锁。保证添加数据安全。

- (void)jj_producer{

    [_testCondition lock];

    self.ticketCount = self.ticketCount + 1;

    NSLog(@"生产一个 现有 count %zd",self.ticketCount);

    [_testCondition unlock];
}

我们同时也给消费者加锁,保证数据安全。如果数据为0,我们就while,知道它不为0。

- (void)jj_consumer{
    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
    }
    
    [_testCondition lock];

    self.ticketCount -= 1;

    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);

    [_testCondition unlock];
}

你们觉得这么做,有没有问题,我们看下打印结果:

20220223143908.jpg

结果发现,数据是不是还是有问题。

所以,我们这里需要添加一下 wait ,和 signal ,来保证条件的安全。

每生产一个,我们都发送一个 signal 来告知,有数据了,你可以消费数据了。

- (void)jj_producer{

    [_testCondition lock];

    self.ticketCount = self.ticketCount + 1;

    NSLog(@"生产一个 现有 count %zd",self.ticketCount);

    [_testCondition signal];

    [_testCondition unlock];
}

我们在消费者那里,如果为 0 ,我们就等待 wait ,等生产发送 signal ,我们就可以直接 -1 操作了。

- (void)jj_consumer{

    [_testCondition lock];

    // 线程安全

    if (self.ticketCount == 0) {

        NSLog(@"等待 count %zd",self.ticketCount);

        // 保证正常流程

        [_testCondition wait];

    }

    //注意消费行为,要在等待条件判断之后

    self.ticketCount -= 1;

    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);

    [_testCondition unlock];

}

看下打印效果:

20220223145525.jpg

NSRecursiveLock

NSRecursiveLock:递归锁,是一种特殊的互斥锁,他和 NSLock 的区别在于,NSRecursiveLock 可以在一个线程中重复加锁,它会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁。也是遵循 NSLocking 协议。

- (void)jj_testRecursive {

    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    static void (^testMethod)(int);

    testMethod = ^(int value){

        if (value > 0) {

            [lock lock];
            NSLog(@"current value = %d",value);
            testMethod(value - 1);
            [lock lock];
        }
    };
    testMethod(10);
}

20220223150931.jpg

NSConditionLock

NSConditionLock:也是一个条件锁,一旦一个线程获得锁,其他线程一定等待。其本质就是 NSCondition + Lock

20220223151148.jpg

  • initWithCondition:初始化条件。

  • lockWhenCondition:加锁,满足条件,就执行,不是这个条件,阻塞线程,好好等待。

  • unlockWithCondition:解锁,更改条件。

举个简单例子来说明一下:

- (void)jj_testConditonLock{

    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];
    });
}

先分析一波:添加一个条件锁,条件初始化为 2 ,然后是3 个异步并发。

其中线程 1 的队列优先级最高,进来就加锁,判断条件是否为 1 ,很明显不是 1 ,所以就等待。

线程 2 进来,发现条件刚好是 2 ,加锁,条件是 2 ,所以会比线程 1 先执行。

线程 3 进来,也是可以做加锁,减锁的操作,看那个先执行加锁操作。

20220223152436.jpg

结果果然是这样,不管我运行多少次,线程 2 都要比线程 1 先执行。

@synchronized

@synchronized 是一个互斥递归锁,虽然性能最低,但是简单易用,容易上手。

这的互斥锁,我就不用上面的售票的例子,也是可以实现一样的效果的。

我们换一个例子:

20220223153037.jpg 看一下线程的信息:

20220223153158.jpg

明显就是多次初始化的时候,在先 release 后retain 多次操作的情况下,出现对象还没有 retain 完,还进行 release 操作导致出错的。

那这里有 2 个方案。 1 个就是使用 atomic 修饰我们的数组。在iOS 10之前 automic 是用的自旋锁, iOS 10 之后使用的是 os_unfair_lock 。也就是在 set 和 get 方法里面,会进行加锁处理。一般我们都不使用。

另外一种方案就是我们的 @synchronized。

- (void)jj_crash{
    for (int i = 0; i < 5000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self) {
                self.testArray = [NSMutableArray array];
            }
        });
    }
}

最后

八大锁已经是介绍完毕了,至于使用那种锁是没有绝对,可灵活运用,如果是读写锁,可以用我们上一篇的提到的栅栏函数进行处理。