美文网首页
iOS中的常见线程锁总结

iOS中的常见线程锁总结

作者: Linghit_iOS | 来源:发表于2020-05-14 14:23 被阅读0次

Created By Kunming

研究背景

我们在开发过程中,为了使应用更加高效、快速地运行,往往我们会使用到多线程来处理复杂的耗时逻辑。但使用了多线程后我们就常会遇到几个线程都要读写同一个资源的情况,若不对共享的资源进行同步限制的话,共享资源就会出现数据错乱的问题。线程锁就是用来处理上述问题的工具,因此掌握各个线程锁的用法及性能差异,在合适的场景下选择好合适的性能锁就显得尤为重要了。

iOS开发中常用的锁

  • @synchronized 关键字加锁
  • NSLock 对象锁
  • NSRecursiveLock 递归锁
  • NSConditionLock 条件锁
  • pthread_mutex 互斥锁
  • dispatch_semaphore 信号量实现加锁
  • OSSpinLock

以下我们会逐一通过示例讲解各个所的用法以及各自的特点

@synchronized 关键字加锁

关键字加锁是iOS下的使用最简单的互斥锁,但是也是性能最差的一个。在通常情况下我们不建议使用。

注:什么是互斥锁?
如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。这种处理方式就叫做互斥锁。

使用方法:
@synchronized(这里添加一个OC对象,一般使用self) {
       这里写要加锁的代码
}

注意点:

  • 加锁的代码尽量少
  • 添加的OC对象必须在多个线程中都是同一对象
  • 优点是不需要显式的创建锁对象,便可以实现锁的机制。
  • @synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

NSLock 对象锁

对象锁属于互斥锁,它使用也比较方便。但是注意不能多次调用lock的方法,否则会造成死锁的问题。

使用方法:
    //主线程中
    NSLock *lock = [[NSLock alloc] init];
        
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        NSLog(@"线程1");
        sleep(2);
        [lock unlock];
        NSLog(@"线程1解锁成功");
    });
        
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        [lock lock];
        NSLog(@"线程2");
        [lock unlock];
    });

注意点:

  • NSLock的接口实际上都是通过NSLocking协议定义的,它定义了lockunlock方法。你使用这些方法来获取和释放该锁。
  • 由于NSLock不能多次调用lock的方法,因此除了NSLocking协议提供的方法外,还提供了tryLocklockBeforeDate两个方法。
tryLock试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回NO。
lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)。

因此NSLock遇到递归等会重复调用lock的方法时,采用tryLocklockBeforeDate时只能避免线程死锁的问题。但NSLock因为没有对实质问题进行处理因此在递归的情况下,使用对象锁仍然是不安全的。

NSRecursiveLock 递归锁

因为研究对象锁时我们指出过NSLock最大的问题是处理递归问题时由于重复调用lock方法导致死锁,递归锁就是专门为了处理递归流程设计的。与对象锁一样递归锁遵循NSLocking协议,并实现了tryLocklockBeforeDate两个方法。

使用方法:
    //创建锁
    _rsLock = [[NSRecursiveLock alloc] init];
    
   //线程1
    dispatch_async(self.concurrentQueue, ^{
        static void(^TestMethod)(int);
        // 递归block
        TestMethod = ^(int value)
        {
            [_rsLock lock];
            if (value > 0)
            {
                [NSThread sleepForTimeInterval:1];
                TestMethod(value--);
            }
            [_rsLock unlock];
        };
        
        TestMethod(5);
    });

注意点:

  • NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。
  • 递归锁会跟踪它被多少次lock。每次成功的lock都必须平衡调用unlock操作。
  • 只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

NSConditionLock条件锁

顾名思义NSConditionLock 实现的需求是根据特定的条件对数据进行上锁和解锁。除了遵循NSLocking协议,并实现了tryLocklockBeforeDate两个方法外,条件锁还提供- (void)lockWhenCondition:(NSInteger)condition- (void)unlockWithCondition:(NSInteger)condition; 方法进行条件解锁。

使用方法:
    //主线程中
    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
        
    //线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lockWhenCondition:1];
        NSLog(@"线程1");
        sleep(2);
        [lock unlock];
    });
        
    //线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:0]) {
            NSLog(@"线程2");
            [lock unlockWithCondition:2];
            NSLog(@"线程2解锁成功");
        }
        else {
            NSLog(@"线程2尝试加锁失败");
        }
    });
        
    //线程3
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // sleep(2);//以保证让线程2的代码后执行
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程3");
            [lock unlock];
            NSLog(@"线程3解锁成功");
        }
        else {
            NSLog(@"线程3尝试加锁失败");
        }
    });
        
    //线程4
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       // sleep(3);//以保证让线程2的代码后执行
        
        [lock lockWhenCondition:2];
        NSLog(@"线程4");
        [lock unlockWithCondition:1];
        NSLog(@"线程4解锁成功");
        
    });

pthread_mutex 互斥锁

第一步:初始化锁属性;第二步:初始化互斥锁,销毁锁属性;第三步:加锁 解锁;第四步:销毁互斥锁

使用方法:
    __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
        
    //线程1
    dispatch_async(self.concurrentQueue), ^{
        pthread_mutex_lock(&mutex);
        NSLog(@"任务1");
        sleep(2);
        pthread_mutex_unlock(&mutex);
    });
        
    //线程2
    dispatch_async(self.concurrentQueue), ^{
        sleep(1);
        pthread_mutex_lock(&mutex);
        NSLog(@"任务2");
        pthread_mutex_unlock(&mutex);
    });

注意点:

  • 申明一个互斥锁,pthread_mutex_t pMutex;

  • 初始化它,pthread_mutex_init(&pMutex,NULL);

  • 使用pMutex之前一定要初始化,否则不生效

  • 获得锁,pthread_mutex_lock(&pMutex);

  • 解锁,pthread_mutex_unlock(&pMutex);

dispatch_semaphore 信号量实现加锁

  • dispatch_semaphore_t dispatch_semaphore_create(long value);
    这个方法就是利用给定的value值创建一个计数信号
    信号值必须是>=0的整数
    当不再使用信号时, 必须调用dispatch_release来释放semaphore对象

  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
    这个方法首先将semaphore对象的信号值-1, 当semaphore对象的信号值<0时, 当前线程锁住(处于休眠状态), 休眠的时间由timeout参数决定.
    DISPATCH_TIME_FOREVER这个宏表示无限期休眠.

  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
    这个方法首先将semaphore对象的信号值+1, 当semaphore对象的信号值>=0时, 当前线程被唤醒, 得以继续执行队列中的其他任务.

使用方法:
#import "NSLockTest.h"
#import <pthread.h>
@interface NSLockTest()
@property (nonatomic,strong) dispatch_semaphore_t semaphore;
@end
@implementation NSLockTest

- (void)forTest
{
    // 创建信号量
    self.semaphore = dispatch_semaphore_create(2);
    
    NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(download1) object:nil];
    [thread1 start];
    
    NSThread *thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(download2) object:nil];
    [thread2 start];
    
    NSThread *thread3 = [[NSThread alloc]initWithTarget:self selector:@selector(download3) object:nil];
    [thread3 start];
    
    NSThread *thread4 = [[NSThread alloc]initWithTarget:self selector:@selector(download4) object:nil];
    [thread4 start];
    
    
}
-(void)download1
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"download1 begin");
    sleep(arc4random()%2);
    NSLog(@"download1 end");
    dispatch_semaphore_signal(self.semaphore);
    [self download1];
}

-(void)download2
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"download2 begin");
    sleep(arc4random()%2);
    NSLog(@"download2 end");
    dispatch_semaphore_signal(self.semaphore);
    [self download2];
}

-(void)download3
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"download3 begin");
    sleep(arc4random()%2);
    NSLog(@"download3 end");
    dispatch_semaphore_signal(self.semaphore);
    [self download3];
}

-(void)download4
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"download4 begin");
    sleep(arc4random()%2);
    NSLog(@"download4 end");
    dispatch_semaphore_signal(self.semaphore);
    [self download4];
}

@end

注意点:

  • dispatch_semaphore_t,使用过信号量来控制线程之间的同步
  • 创建计数信号的时候传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

OSSpinLock 不再安全

OSSpinLock 已经不建议使用了,因为经过大神验证OSSpinLock已经不再可靠。
主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

为什么忙等会导致低优先级线程拿不到时间片?这还得从操作系统的线程调度说起。

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。上面说到由于高优先级线程占用了大量时间片,没有再分配到时间片的低优先级线程就无法释放锁。

iOS开发中自旋和互斥锁的区别

相同点:

都能保证同一时间只有一个线程访问共享资源。都能保证线程安全。

不同点:

互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。

自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。

自旋锁的效率高于互斥锁。

使用自旋锁时要注意:

由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。

两种锁的加锁原理:

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换(主动出让时间片,线程休眠,等待下一次唤醒),cpu的抢占,信号的发送等开销。

自旋锁:线程一直是running(加锁——>解锁),死循环(忙等 do-while)检测锁的标志位,机制不复杂。

锁的选型

OSSPinLock,dispatch_semaphore的性能远远优于其他的锁,但是OSSpinLock由于优先级反转的问题,苹果在iOS 10的时候推出了os_unfair_lock来替代,而且性能不减当年,但是要在iOS 10之后才能用(虽然自旋锁的性能优于互斥锁),可以看出@synchronize和NSConditionLock明显性能最差,如果项目中对性能特别敏感,建议使用dispatch_semaphore,如果基于方便的话就用@synchronize就可以了

相关文章

  • OC--各种线程锁

    参考:正确使用多线程同步锁@synchronized()iOS中的锁iOS多线程安全详解iOS 常见知识点(三):...

  • iOS中的常见线程锁总结

    Created By Kunming 研究背景 我们在开发过程中,为了使应用更加高效、快速地运行,往往我们会使用到...

  • iOS中的锁

    锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...

  • iOS开发中的锁

    iOS开发中的锁 本人对锁没有深入理解,只是看了几篇文章,在这里做一下简单的总结。 iOS开发中,锁是用来解决线程...

  • iOS 多线程

    参考链接 iOS多线程iOS 多线程:『GCD』详尽总结iOS简单优雅的实现复杂情况下的串行需求(各种锁、GCD ...

  • OC-锁的介绍及简单使用

    1、@synchronized @synchronized是 iOS 中最常见的锁,用法很简单: 在线程 1 内容...

  • 线程安全: 互斥锁和自旋锁(10种)

    无并发,不编程.提到多线程就很难绕开锁?. iOS开发中较常见的两类锁: 1. 互斥锁: 同一时刻只能有一个线程获...

  • iOS锁

    常见锁代码 多线程场景不可避免需要使用锁来解决数据竞争、并发访问等问题,iOS 开发中,你会接触到各种各样的锁,不...

  • 起底多线程同步锁(iOS)

    起底多线程同步锁(iOS) 起底多线程同步锁(iOS)

  • iOS 多线程

    iOS中的各种锁iOS多线程到底不安全在哪里?

网友评论

      本文标题:iOS中的常见线程锁总结

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