美文网首页
iOS 中锁的应用

iOS 中锁的应用

作者: Queen_BJ | 来源:发表于2020-03-30 16:03 被阅读0次
    锁的应用是为了保证线程安全

    多个线程访问同一块资源的时候,很容易引发数据混乱问题

    基本概念
    • 自旋锁
      用于线程同步,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待(所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁)一旦获取了自旋锁,线程会一直保持该锁,直至释放。自旋锁避免了上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的
    • 互斥锁
      用于多线程中,防止两条线程同时对同意公共资源进行读写的机制。
      在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。
    两种锁的应用

    互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
    1 临界区有IO操作
    2 临界区代码复杂或者循环量大
    3 临界区竞争非常激烈
    4 单核处理器
    至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。

    • 条件锁
      就是条件变量,当进程的某些资源不满足时进入休眠。当分配到资源后,条件锁打开
    • semaphore 一种更高级的同步机制。互斥锁可以说是semaphore在仅取值0/1时的特例,信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
    一、互斥锁

    1、 @synchronized :
    @synchronized (self) { //􏴉􏰼􏴧􏳋􏰑􏴩􏴪􏴉􏰼􏴧􏳋􏰑􏴩􏴪 需要锁定的代码 }
    @synchronized 的作用是创建一个互斥锁,防止self对象在同一时间内被其它线程访问,起到线程的保护作用。
    ()内可以是任何的Objective-C对象

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [self synchronized];
    }
    
    - (void)synchronized {
        NSObject * cjobj = [NSObject new];
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            @synchronized(cjobj){
                NSLog(@"线程1开始");
                sleep(3);
                NSLog(@"线程1结束");
            }
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);
            @synchronized(cjobj){
                NSLog(@"线程2");
            }
        });
    }
    
    控制台输出:
    2017-10-18 11:35:13.459194+0800 Thread-Lock[24855:431100] 线程1开始
    2017-10-18 11:35:16.460210+0800 Thread-Lock[24855:431100] 线程1结束
    2017-10-18 11:35:16.460434+0800 Thread-Lock[24855:431101] 线程2
    

    注意:@synchronized(cjobj) 指令使用的 cjobj 为该锁的唯一标识,只有当标识相同时,才为满足互斥,
    如果线程 2 中的 @synchronized(cjobj) 改为 @synchronized(self) ,那么线程 2 就不会被阻塞
    优点:不需要在代码中显式的创建锁对象,便可以实现锁的机制
    缺点:@synchronized 块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

    2、NSLock

    NSLock * cjlock = [NSLock new];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [cjlock lock];
        NSLog(@"线程1加锁成功");
        sleep(2);
        [cjlock unlock];
        NSLog(@"线程1解锁成功");
      });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [cjlock lock];
        NSLog(@"线程2加锁成功");
        [cjlock unlock];
        NSLog(@"线程2解锁成功");
      });
    
    2020-03-28 15:56:57.580346+0800 iOSLockDemo[71692:3779253] 线程1加锁成功
    2020-03-28 15:56:59.584020+0800 iOSLockDemo[71692:3779253] 线程1解锁成功
    2020-03-28 15:56:59.584024+0800 iOSLockDemo[71692:3779258] 线程2加锁成功
    2020-03-28 15:56:59.584163+0800 iOSLockDemo[71692:3779258] 线程2解锁成功
    

    tryLock 返回 YES NO

       NSLock * cjlock = [NSLock new];
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [cjlock lock];
            NSLog(@"线程1加锁成功");
            sleep(2);
            [cjlock unlock];
            NSLog(@"线程1解锁成功");
        });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if ([cjlock tryLock]) {
                NSLog(@"线程3加锁成功");
                [cjlock unlock];
                NSLog(@"线程3解锁成功");
            }else {
                NSLog(@"线程3加锁失败");
            }
        });
    2020-03-28 16:08:23.535844+0800 iOSLockDemo[71997:3790084] 线程1加锁成功
    2020-03-28 16:08:23.535844+0800 iOSLockDemo[71997:3790083] 线程3加锁失败
    2020-03-28 16:08:25.539981+0800 iOSLockDemo[71997:3790084] 线程1解锁成功
    

    lockBeforeDate

    NSLock * cjlock = [NSLock new];
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [cjlock lock];
        NSLog(@"线程1加锁成功");
        sleep(2);
        [cjlock unlock];
        NSLog(@"线程1解锁成功");
      });
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([cjlock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
          NSLog(@"线程5加锁成功");
          [cjlock unlock];
          NSLog(@"线程5解锁成功");
        }else {
          NSLog(@"线程5加锁失败");
        }
      });
    
    2020-03-28 16:06:39.145099+0800 iOSLockDemo[71974:3788856] 线程1加锁成功
    2020-03-28 16:06:41.149201+0800 iOSLockDemo[71974:3788856] 线程1解锁成功
    2020-03-28 16:06:41.149202+0800 iOSLockDemo[71974:3788857] 线程5加锁成功
    2020-03-28 16:06:41.149957+0800 iOSLockDemo[71974:3788857] 线程5解锁成功
    

    总结

    • 除 lock 和 unlock 方法外,NSLock 还提供了 tryLock 和 lockBeforeDate:两个方法
    • 由上面的结果可以看到 tryLock 并不会阻塞线程,[cjlock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码
    • lockBeforeDate: 方法会在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回 YES

    3、thread_mutex 与 pthread_mutex(recursive):互斥锁(C语言)
    pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,POSIX 互斥锁是一种超级易用的互斥锁,
    使用的时候:
    只需要使用 pthread_mutex_init 初始化一个 pthread_mutex_t,
    pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定 ,
    pthread_mutex_unlock 来解锁,
    pthread_mutex_destroy 来销毁锁。

    导出#import <pthread.h>
    #pragma mark -- pthread_mutex_t
    -(void)pthread_mutex_twithMethod
    {
        __block pthread_mutex_t cjlock;
        pthread_mutex_init(&cjlock, NULL);
       dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            pthread_mutex_lock(&cjlock);
            NSLog(@"线程1开始");
            sleep(3);
            NSLog(@"线程1结束");
            pthread_mutex_unlock(&cjlock);
            
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);
            pthread_mutex_lock(&cjlock);
            NSLog(@"线程2");
            pthread_mutex_unlock(&cjlock);
        });
    }
    2020-03-28 17:07:14.440619+0800 iOSLockDemo[72429:3813295] 线程1开始
    2020-03-28 17:07:17.443088+0800 iOSLockDemo[72429:3813295] 线程1结束
    2020-03-28 17:07:17.443386+0800 iOSLockDemo[72429:3813294] 线程2
    
    
    二、自旋锁 OSSpinLock

    自选锁,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。

    SSpinLock lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&lock);
    ...
    OSSpinLockUnlock(&lock);
    

    上面是OSSpinLock使用方式,编译会报警告,已经废弃了
    os_unfair_lock:(互斥锁)
    os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用。os_unfair_lock是一种互斥锁,它不会向自旋锁那样忙等,而是等待线程会休眠。

    //初始化
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    //加锁
    os_unfair_lock_lock(&lock);
    //解锁
    os_unfair_lock_unlock(&lock);
    
    #import "os_unfair_lockDemo.h"
    #import <os/lock.h>
    @interface os_unfair_lockDemo()
    @property (assign, nonatomic) os_unfair_lock ticketLock;
    @end
    
    @implementation os_unfair_lockDemo
    - (instancetype)init
    {
    self = [super init];
    if (self) {
    self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
    }
    
    
    //卖票
    - (void)sellingTickets{
    os_unfair_lock_lock(&_ticketLock);
    
    [super sellingTickets];
    
    os_unfair_lock_unlock(&_ticketLock);
    }
    @end
    
    三、递归锁 NSRecursiveLock

    允许同一个线程对同一把锁进行重复加锁。要考重点同一个线程和同一把锁

    -(void)NSRecursiveLockMethod
    {
        NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            
            static void (^RecursiveMethod)(int);
            RecursiveMethod = ^(int value) {
                
                [lock lock];
                if (value > 0) {
                    
                    NSLog(@"value = %d", value);
                    sleep(2);
                    RecursiveMethod(value - 1);
                }
                [lock unlock];
            };
            
            RecursiveMethod(10);
        });
    }
    

    写法可以和NSLock一模一样,NSLock和NSRecursiveLock的区别
    NSLock lock了之后,没有unlock那么会发生死锁。
    允许同一线程多次加锁,而不会造成死锁,但是没有及时unlock,是会导致其他线程阻塞的,还是得记得unlock。

    四、条件锁

    根据一定条件满足后进行 加锁/解锁.

    @interface NSConditionLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    //初始化一个NSConditionLock对象
    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
    
    @property (readonly) NSInteger condition;  //锁的条件
    
    //满足条件时加锁
    - (void)lockWhenCondition:(NSInteger)condition;
    
    
    - (BOOL)tryLock;
    //如果接收对象的condition与给定的condition相等,则尝试获取锁,不阻塞线程
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    
    //解锁后,重置锁的条件
    - (void)unlockWithCondition:(NSInteger)condition;
    
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    //在指定时间前尝试获取锁,若成功则返回YES 否则返回NO
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    
    //主线程中
        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解锁成功");
            
        });
    

    打印结果

    2020-03-28 17:53:45.896078+0800 iOSLockDemo[72828:3836240] 线程3尝试加锁失败
    2020-03-28 17:53:46.900668+0800 iOSLockDemo[72828:3836241] 线程2
    2020-03-28 17:53:46.900914+0800 iOSLockDemo[72828:3836241] 线程2解锁成功
    2020-03-28 17:53:46.900917+0800 iOSLockDemo[72828:3836242] 线程4
    2020-03-28 17:53:46.901014+0800 iOSLockDemo[72828:3836242] 线程4解锁成功
    2020-03-28 17:53:46.901031+0800 iOSLockDemo[72828:3836243] 线程1
    

    结果说明:
    1 初始化一个条件锁,条件为0
    2 由于线程1 和线程4条件不满足,所以循环一段时间休眠,等待满足条件满足时唤醒;线程3尝试加锁,不会阻塞线程,但是条件不满足所以直接休眠;线程2休眠1秒后尝试加锁。满足条件所以加锁成功;
    3 线程2伴随重置加锁条件2进行解锁;
    4 此时线程4满足条件,系统唤醒进行加锁,并且重置加锁条件14
    5 此时线程1满足条件,系统唤醒进行加锁,并且解锁,此时条件为1

    各种锁的性能比较

    屏幕快照 2020-03-26 下午5.46.34.png
    注意:
    1.这个数字仅仅代表每次加解锁的耗时,并不能全方面的代表性能
    2.不同的机型和系统,不同的循环次数可能结果会略微有些差异
    但是还是可以看出@synchronized:是表现最差的。

    五、死锁

    死锁就是队列引起的循环等待
    1、一个比较常见的死锁例子:主队列同步

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSLog(@"1");// 任务1
        dispatch_sync(dispatch_get_main_queue(), ^{
           
           NSLog(@"2");// 任务2
        });
      
      NSLog(@"3");// 任务3
    }
    
    结果输出1
    

    分析

    • dispatch_sync表示是一个同步线程;
    • dispatch_get_main_queue表示运行在主线程中的主队列;
    • 任务2是同步线程的任务。
      首先执行任务1,接下来,程序遇到了同步线程,那么它会进入等待,等待任务2执行完,然后执行任务3。
      但这是队列,有任务来,当然会将任务加到队尾,然后遵循FIFO原则执行任务,那么,现在任务2就会被加到最后,任务3排在了任务2前面,任务3要等任务2执行完才能执行,任务2由排在任务3后面,意味着任务2要在任务3执行完才能执行,所以他们进入了互相等待的局面

    想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。

    2、同样,下边的代码也会造成死锁:

    dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(serialQueue, ^{
           
            dispatch_sync(serialQueue, ^{
                
                NSLog(@"deadlock");
            });
        });
    

    外面的函数无论是同步还是异步都会造成死锁。
    **这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁
    解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决

        dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
        
        dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(serialQueue, ^{
           
            dispatch_sync(serialQueue2, ^{
                
                NSLog(@"deadlock");
            });
        });
    

    这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。

    3、NSLock死锁

    -(void)methodA
    {
       self.lock = [[NSLock alloc]init];
      [self.lock lock];
      NSLog(@"1");
      [self methodB];
      NSLog(@"2");
      [self.lock unlock];
    }
    -(void)methodB
    
    {
      [self.lock lock];
      NSLog(@"3");
      [self.lock unlock];
    }
    
    2020-09-25 16:40:48.177198+0800 XXX TEST  [2678:167611] 1
    
    

    输出结果:1
    原因如下:由于当前线程运行到第一个lock加锁,现在再次运行到lock同样的锁,需等待当前线程解锁,把当前线程挂起,不能解锁
    NSLock是非递归锁,当同一线程重复获取同一非递归锁时,就会发生死锁
    解决办法:
    我们可以用NSRecursiveLock或者@synchronized替代NSLock
    因为NSRecursiveLock是递归锁,@synchronized是同步互斥锁
    递归锁:它允许同一线程多次加锁,而不会造成死锁。

    -(void)methodA
    {
       self.lock = [[NSLock alloc]init];
      [self.lock lock];
      NSLog(@"1");
      [self methodB];
      NSLog(@"2");
      [self.lock unlock];
    }
    -(void)methodB
    {
        @synchronized (self) {
            
            NSLog(@"3");
        }
    或
     NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
        [lock lock];
        NSLog(@"3");
       [lock unlock];
    
    //  [self.lock lock];
    //  NSLog(@"3");
    //  [self.lock unlock];
    }
    

    信号量Dispatch Semaphore 也是锁

    GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。
    Dispatch Semaphore 提供了三个函数

    • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
    • dispatch_semaphore_signal:发送一个信号,让信号总量加1
    • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

    Dispatch Semaphore 在实际开发中主要用于:

    • 保持线程同步,将异步执行任务转换为同步执行任务
    • 保证线程安全,为线程加锁
    1、保持线程同步:
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
        __block NSInteger number = 0;
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            
            number = 100;
            
            dispatch_semaphore_signal(semaphore);
        });
        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        
        NSLog(@"semaphore---end,number = %zd",number);
    

    dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后当前线程继续执行

    可参考内容

    2、保证线程安全,为线程加锁:

    在线程安全中可以将dispatch_semaphore_wait看作加锁,而dispatch_semaphore_signal看作解锁
    首先创建全局变量

     _semaphore = dispatch_semaphore_create(1);
    

    注意到这里的初始化信号量是1。

    - (void)asyncTask
    {
        
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        
        count++;
        
        sleep(1);
        
        NSLog(@"执行任务:%zd",count);
        
        dispatch_semaphore_signal(_semaphore);
    }
    

    异步并发调用asyncTask

    for (NSInteger i = 0; i < 100; i++) {
            
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                
                [self asyncTask];
            });
        }
    

    然后发现打印是从任务1顺序执行到100,没有发生两个任务同时执行的情况。
    原因如下:
    在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。

    自旋锁优缺点

    自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
    自旋锁的缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用

    参考一

    相关文章

      网友评论

          本文标题:iOS 中锁的应用

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