• iOS: 线程中那些常见的锁


    一、介绍

    在多线程开发中,锁的使用基本必不可少,主要是为了解决资源共享时出现争夺而导致数据不一致的问题,也就是线程安全问题。锁的种类很多,在实际开发中,需要根据情况选择性的选取使用,毕竟使用锁也是消耗CPU的。 本人虽然一直有使用多线程进行开发,但是对于锁的使用和理解并不是特别的深入,这不看到一篇挺mark的博客:https://www.jianshu.com/p/a236130bf7a2,在此基础上稍添加点东西转载过来(尊重原创),一是为了记录便于随时翻阅,而是为了写一遍加深印象,知识都是一个copy和attract的过程。 

    二、种类

    1、互斥锁

    概念:对共享数据进行锁定,保证同一时刻只能有一个线程去操作。 

    • 抢到锁的线程先执行,没有抢到锁的线程就会被挂起等待。
    • 等锁用完后需要释放,然后其它等待的线程再去抢这个锁,那个线程抢到就让那个线程再执行。
    • 具体哪个线程抢到这个锁是由cpu调度决定的。

    常用:

    @synchronized:同步代码块

    example:执行操作

    /**
     *设置属性值
     */
    -(void)setMyTestString:(NSString *)myTestString{
        @synchronized(self) {
            // todo something
            _myTestString = myTestString;
        }
    }

    example:创建单例

    //注意:此时为了保证单例模式的更加严谨,需要重写`allocWithZone`方法,保证其他开发者使用`alloc`和`init`方法时,不再创建新的对象。必要的时候还需要重写`copyWithZone`方法防止`copy`属性对单例模式的影响。 iOS中还有一种更加轻便的方法实现单例模式,即使用GCD中的dispatch_once函数实现。
    +(instancetype)shareInstance{ // 1.定义一个静态实例,初值nil static TestSynchronized *myClass = nil; // 2.添加同步锁,创建实例 @synchronized(self) { // 3.判断实例是否创建过,创建过则退出同步锁,直接返回该实例 if (!myClass) { // 4.未创建过,则新建一个实例并返回 myClass = [[self alloc] init]; } } return myClass; }

    NSLock:不能迭代加锁,如果发生两次lock,而未unlock过,则会产生死锁问题。

    example:执行操作

    ///定义一个静态锁变量, lock--unlock 、tryLuck---unLock  必须成对存在
    static NSLock *mylock;
    -(void)viewDidLoad {
        [super viewDidLoad];
        mylock = [[NSLock alloc] init];
    }
    
    //当前线程锁失败,也可以继续其它任务,用 trylock 合适
    -(void)myLockTest1{
        if ([mylock tryLock]) {
            // to do something
            [mylock unlock];
        }
    }
    
    //当前线程只有锁成功后,才会做一些有意义的工作,那就lock,没必要轮询trylock
    -(void)myLockTest2{
        [mylock lock];
        // to do something
        [mylock unlock];
    }

    2、递归锁

    概念:递归锁可以被同一线程多次请求,而不会引起死锁,即在多次被同一个线程进行加锁时,不会造成死锁,这主要是用在循环或递归操作中。

    • 递归锁会跟踪它被lock的次数,每次成功的lock都必须平衡调用unlock操作。
    • 只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

    常用:

    NSRecursiveLock: 递归锁

    example: 异步执行block

    //创建递归锁
    NSRecursiveLock *myRecursiveLock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            static void (^MyRecursiveLockBlock)(int value);
            MyRecursiveLockBlk = ^(int value){
                [myRecursiveLock lock];
                if (value > 0) {
                    // to do something
                    NSLog(@"MyRecursiveLockBlk value = %d", value);
                    MyRecursiveLockBlock(value - 1);
                }
                [myRecursiveLock unlock];
            };
            MyRecursiveLockBlock(6);
    });
    
    //注意:此时如果将例程中的递归锁换成互斥锁:
    //NSRecursiveLock *myRecursiveLock = [[NSRecursiveLock alloc] init];换成
    //NSLock *myLock = [[NSLock alloc] init];,则会发生死锁问题。

    3、读写锁

    概念:读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

    • 读写锁将访问者分为读出写入两种,当读写锁在读加锁模式下,所有以读加锁方式访问该资源时,都会获得访问权限,而所有试图以写加锁方式对其加锁的线程都将阻塞,直到所有的读锁释放。

    • 当在写加锁模式下,所有试图对其加锁的线程都将阻塞。

    常用:

    pthread_rwlock_t(读写锁)、 pthread_rwlock_wrlock(写锁)、 pthread_rwlock_rdlock(读锁)

    example: 异步读写数据

    #import "ViewController.h"
    #import <pthread.h>
    @interface ViewController ()
    @property(nonatomic, copy) NSString *rwStr;
    @end
    
    @implementation ViewController
    
    ///全局的读写锁
    pthread_rwlock_t rwlock;
    
    -(void)viewDidLoad {
        [super viewDidLoad];
        // 初始化读写锁
        pthread_rwlock_init(&rwlock,NULL);
        __block int i;
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            i = 5;
            while (i>=0) {
                NSString *temp = [NSString stringWithFormat:@"writing == %d", i];
                [self writingLock:temp];
                i--;
            }  
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            i = 5;
            while (i>=0) {
                [self readingLock];
                i--;
            }
        });
    }
    // 写加锁
    -(void)writingLock:(NSString *)temp{
        pthread_rwlock_wrlock(&rwlock);
        // writing
        self.rwStr = temp;
        NSLog(@"%@", temp);
        pthread_rwlock_unlock(&rwlock);
    }
    // 读加锁
    -(NSString *)readingLock{
        pthread_rwlock_rdlock(&rwlock);
        // reading
        NSString *str = self.rwStr;
        NSLog(@"reading == %@",self.rwStr);
        pthread_rwlock_unlock(&rwlock);
        return str;
    }
    @end

      

    4、自旋锁

    概念:它是一种忙等的锁,适用于轻量访问。自旋锁是非阻塞的,当一个线程无法获取自旋锁时,会自旋,直到该锁被释放,等待的过程中线程并不会挂起。(实质上就是,如果自旋锁已经被别的执行单元保持,调用者就一直循环在等待该自旋锁的保持着已经释放了锁)。

    • 自旋锁的使用者一般保持锁的时间很短,此时其效率远高于互斥锁

    • 自旋锁保持期间是抢占失效的

    • 优点:不用进行线程的切换    
    • 缺点:如果一个线程霸占锁的时间过长,自旋会消耗CPU资源

    常用:

    OSSpinLock:自旋锁

    example: 执行操作

    // 头文件
    #import <libkern/OSAtomic.h>
    // 初始化自旋锁
    static OSSpinLock myLock = OS_SPINLOCK_INIT;
    // 自旋锁的使用
    -(void)SpinLockTest{
        OSSpinLockLock(&myLock);
        // to do something
        OSSpinLockUnlock(&myLock);
    }

    5、分布锁

    概念:跨进程的分布式锁是进程间同步的工具,底层是用文件系统实现的互斥锁,并不强制进程休眠,而是起到告知的作用。

    • 它没有实现NSLocking协议,所以没有会阻塞线程的lock方法,取而代之的是非阻塞的tryLock方法来获取锁,用unLock方法释放锁。
    • 如果一个获取锁的进程在释放锁之前就退出了,那么锁就一直不能释放,此时可以通过breakLock强行获取锁。

    常用:

    NSDistributedLock:自旋锁

    example: 执行操作

    //给文件创建分布锁
    NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
    while (![lock tryLock])
    {
        sleep(1);
    }
    
    //do something
    
    [lock unlock];
    
    //但在实际使用过程中,当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.这是使用NSDistributedLock时非常隐蔽的风险.其//实要解决的问题就是如何在进程退出时会自动释放锁.

    6、条件变量

    概念:与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。

    • 一个线程需要等待某一条件出现才能继续执行,而这个条件是由别的线程产生的,这个时候就用到条件变量。常见的情况是:生产者-消费者问题。

    • 条件变量可以让一个线程等待某一条件,当条件满足时,会收到通知。在获取条件变量并等待条件发生的过程中,也会产生多线程的竞争,所以条件变量通常和互斥锁一起工作。 

    常用:

    NSCondition:是互斥锁和条件锁的结合,即一个线程在等待signal而阻塞时,可以被另一个线程唤醒,由于操作系统实现的差异,即使没有发送signal消息,线程也有可能被唤醒,所以需要增加谓词变量来保证程序的正确性。

    example: 执行操作

    // 创建锁
    NSCondition *condition = [[NSCondition alloc] init];
    static int count = 0;
    // 生产者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while(count<20)
        {
           [condition lock];
           // 生产
           count ++;
           NSLog(@"生产 = %d",count);
           [condition signal];
           [condition unlock];
        }
    });
    // 消费者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (count>0)
        {
            [condition lock];
            // 消耗
            count --;
            NSLog(@"消耗剩余 = %d",count);
            [condition unlock];
        }
    });

    NSConditionLock:与NSCondition的实现机制不一样,当定义的条件成立的时候会获取锁,反之,释放锁。

    example: 执行操作

    // 创建锁
    NSConditionLock *condLock = [[NSConditionLock alloc] initWithCondition:ConditionHASNOT];
    static int count = 0;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 生产者
        while(true)
        {
             [condLock lock];
             // 生产
             count ++;
             [condLock unlockWithCondition:ConditionHAS];
        }
    }
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 消费者
        while (true)
        {
             [condLock lockWhenCondition:ConditionHAS];
             // 消耗
             count --;
             [condLock unlockWithCondition:(count<=0 ? ConditionHASNOT : ConditionHAS)];
        }
    }

      

    7、信号量

    概念:信号量是一个计数器,常用于处理进程或线程的同步问题,特别是对临界资源的同步访问。临界资源可以简单的理解为在某一时刻只能由一个进程或线程进行操作的资源,这里的资源可以是一段代码、一个变量或某种硬件资源。

    • 信号量:可以是一种特殊的互斥锁,可以是资源的计数器
    • 可以使用GCD中的Dispatch Semaphore实现,Dispatch Semaphore是持有计数的信号,该计数是多线程编程中的计数类型信号。计数为0时等待,计数大于等于1时,减1为不等待。

    常用: 

    dispatch_semaphore_t(信号)、dispatch_semaphore_signal(持有信号)、diapatch_semaphore_wait(释放信号)

    example: 执行操作

    //创建信号
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    [self getTasksWithCompletionHandler:^ {
    
        //doing something
    
        //持有信号
         dispatch_semaphore_signal(semaphore);
    }];
    
    //释放信号
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

     

    8、栅栏/屏障(barrier)

    概念:dispatch_barrier_async函数的作用与barrier的意思相同,在进程管理中起到一个栅栏的作用,它等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,该函数需要同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用。

    • 栅栏必须单独执行,不能与其他任务并发执行,栅栏只对并发队列有意义。
    • 栅栏只有等待当前队列所有并发任务都执行完毕后,才会单独执行,带起执行完毕,再按照正常的方式继续向下执行。

    常用:

    dispatch_barrier_async:异步栅栏函数

    example: 多读单写(读读并发、读写互斥、写写互斥)

    - (id)objectForKey:(NSString *)key {
        
         __block id obj;
         //同步读取指定数据  
         dispatch_sync(concurrent_queue, ^{
              obj = [userCenterDic objectForKey:key];      
         });
    
         return obj;
    }    
    
    
    -(void)setObject:(id )obj foeKey:(NSString *)key{
          
         //异步栅栏调用设置数据
         dispatch_async(concurrent_queue, ^{
               [userCenterDic setObject:obj forKey:key];      
         });
    }

    9、pthread_mutex

    概念:C语言定义下的多线程加锁方式,在很多OC对象的底层结构中,可以看到pthread_mutex使用的还是很受苹果官方推荐的。

    用法:

    • pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t attr); 初始化锁变量mutex,attr为锁属性,NULL值为默认属性。
    • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 宏初始化锁变量mutex

    • pthread_mutexattr_settype(pthread_mutexattr_t attr,  int type); 设置锁类型
    • pthread_mutex_lock(pthread_mutex_t* mutex);加锁
    • pthread_mutex_tylock(pthread_mutex_t* mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。
    • pthread_mutex_unlock(pthread_mutex_t* mutex);释放锁
    • pthread_mutex_destroy(pthread_mutex_t* *mutex);使用完后释放

    常用:

    pthread_mutex:互斥锁

    example: 

    //创建锁
    __block pthread_mutex_t theLock; pthread_mutex_init(
    &theLock, NULL);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
    0), ^{ pthread_mutex_lock(&theLock); NSLog(@"需要线程同步的操作1 开始"); sleep(3); NSLog(@"需要线程同步的操作1 结束"); pthread_mutex_unlock(&theLock); }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1); pthread_mutex_lock(&theLock); NSLog(@"需要线程同步的操作2"); pthread_mutex_unlock(&theLock); });

    pthread_mutex(recursive):递归锁

    example: 

    //注意:这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。
    //如果使用pthread_mutex_init(&theLock, NULL)初始化锁的话,下面的代码会出现死锁现象,但是改成使用递归锁的形式,则没有问题。
    
    //创建锁 
    __block pthread_mutex_t theLock;
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置成递归类型
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
        
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{   
        static void (^RecursiveMethod)(int);
        RecursiveMethod = ^(int value) {
            pthread_mutex_lock(&theLock);
            if (value > 0) {
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveMethod(value - 1);
            }
            pthread_mutex_unlock(&theLock);
       };
       RecursiveMethod(5);
    });

    三、性能

    点击这里,参考网址

    • No1.自旋锁OSSpinLock耗时最少
    • No2.pthread_mutex
    • No3.NSLock、NSCondition、NSRecursiveLock耗时接近
    • No4. @synchronized
    • No5. NSConditionLock
    • 栅栏的性能并没有很好,在实际开发中也很少用到。

    自旋锁是线程不安全的在 ibireme 的 不再安全的 OSSpinLock有解释,进一步的ibireme在文中也有提到苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大。
  • 相关阅读:
    扩展KMP学习笔记
    【洛谷P5555】秩序魔咒【回文自动机】
    PAM(回文自动机)学习笔记
    形象理解转置原理在FFT中的应用
    NOIP2020考后总结与计划
    CSP2020游记
    JavaScript——面向对象编程
    JavaScript——实现继承的几种方式
    JavaScript闭包
    学习一门新编程语言的6个步骤
  • 原文地址:https://www.cnblogs.com/XYQ-208910/p/11732266.html
Copyright © 2020-2023  润新知