美文网首页
iOS 开发常见的几种锁

iOS 开发常见的几种锁

作者: 远方竹叶 | 来源:发表于2020-11-04 18:23 被阅读0次

    简介

    在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题。我们常常会使用一些锁来保证程序的线程安全,保证每次只有一个线程访问这一块资源。

    多线程编程中,应该尽量避免资源在线程之间共享,以减少线程间的相互作用。

    锁的分类

    锁住要分为以下几类:

    互斥锁

    它将代码切片成为一个个代码块,使得当一个代码块在运行时,其他线程不能运行他们之中的任意片段,只有等到该片段结束运行后才可以运行。通过这种方式来防止多个线程同时对某一资源进行读写的一种机制。常用的有:

    • @synchronized

    • NSLock

    • pthread_mutex

    自旋锁

    多线程同步的一种机制,当其检测到资源不可用时,会保持一种“忙等”的状态,直到获取该资源。它的优势在于避免了上下文的切换,非常适合于堵塞时间很短的场合;缺点则是在“忙等”的状态下会不停的检测状态,会占用 cpu 资源。常用的有:

    • OSSpinLock

    • atomic

    条件锁

    通过一些条件来控制资源的访问,当然条件是会发生变化的。常用的有:

    • NSCondition

    • NSConditionLock

    信号量

    是一种高级的同步机制。互斥锁可以认为是信号量取值0/1时的特例,可以实现更加复杂的同步。常用的有:

    • dispatch_semaphore

    递归锁

    它允许同一线程多次加锁,而不会造成死锁。递归锁是特殊的互斥锁,主要是用在循环或递归操作中。常用的有:

    • pthread_mutex(recursive)

    • NSRecursiveLock

    读写锁

    是并发控制的一种同步机制,也称“共享-互斥锁”,也是一种特殊的自旋锁。它把对资源的访问者分为读者和写者,它允许同时有多个读者访问资源,但是只允许有一个写者来访问资源。常用的有:

    • pthread(rwlock)

    • dispatch_barrier_async / dispatch_barrier_sync

    常见几种锁的使用方法

    OSSpinLock(iOS 10 以后废弃)

    它是一种自旋锁,只有加锁,解锁,尝试加锁三个方法,其中尝试加锁是非线程阻塞的。通过导入 #import <libkern/OSAtomic.h> 引入并调用,使用示例:

        OSSpinLock lock = OS_SPINLOCK_INIT;
        OSSpinLockLock(&lock);
        //执行代码
        OSSpinLockUnlock(&lock);
    

    OSSpinLock 有可能会造成死锁,不再安全的锁:

    有可能在优先级比较低的线程里对共享资源加锁了,然后高优先级的线程抢占了低优先级的调用 cpu 时间,导致高优先级的线程一直在等待低优先级的线程释放锁,然而低优先级的线程根本没法抢占高优先级的 cpu 时间。(优先级反转)

    NSLock

    是一种互斥锁,在 Cocoa 程序下 所有锁(包括 NSLockNSConditionNSRecursiveLockNSConditionLock) 的接口实际上是通过 NSLocking 协议定义的,它定义了 lockunlock 方法,使用这些方法来获取和释放该锁。

    @protocol NSLocking
    
    - (void)lock;
    - (void)unlock;
    
    @end
    

    此外,NSLock 类还添加了如下方法:

    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    • tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回 NO
    • lockBeforeDate: 方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)

    NSRecursiveLock

    是一个递归锁。NSRecursiveLock 类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它被多少次 lock。每次成功的 lock 都必须平衡调用unlock 操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。在使用锁时最容易犯的一个错误就是在递归或循环中造成死锁,如下代码:

    //创建锁
    NSLock *lock = [[NSLock alloc] init];
    
    //线程1
    dispatch_async(dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT), ^{
        static void(^TestBlock)(int);
        TestBlock = ^(int value) {
            NSLog(@"加锁: %d",value);
            [lock lock];
            if (value > 0) {
                TestBlock(--value);
            }
            NSLog(@"程序退出!");
            [lock unlock];
        };
        
        TestBlock(5);
    });
    

    在线程1中的递归 block 中,锁会被多次的 lock,所以自己也被阻塞了。此处将NSLock 换成 NSRecursiveLock,便可解决问题。

    NSRecursiveLock *rslock = [[NSRecursiveLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^TestBlock)(int);
        TestBlock = ^(int value) {
            NSLog(@"加锁: %d",value);
            [rslock lock];
            if (value > 0) {
                TestBlock(--value);
            }
            
            NSLog(@"程序退出!");
            [rslock unlock];
        };
    
        TestBlock(5);
    });
    

    NSCondition

    NSCondition 的对象实际上是作为一个锁和线程检查器,锁主要是为了检测条件时保护数据源,执行条件引发的任务。线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

    NSCondition 除了 lockunlock 方法来使用解决线程同步问题,还提供了更高级的用法:

    - (void)wait; //让当前线程处于等待状态
    - (BOOL)waitUntilDate:(NSDate *)limit; //让当前线程处于等待到什么时间
    - (void)signal; // CPU发信号告诉正在等待中的线程不用在等待,可以继续执行(只对一个线程起作用)
    - (void)broadcast;// 通知所有在等在等待中的线程(广播)
    
    @property (nullable, copy) NSString *name;
    
    • wait 堵塞当前线程,使线程进入休眠,等待唤醒信号。
    • waitUntilDate 堵塞当前线程,线程进入休眠,等待唤醒信号或者超时。如果是被信号唤醒返回 YES,否者返回 NO
    • signal 唤醒一个正在休眠的线程,如果要唤醒多个线程,需要调用多次,如果没有线程在等待,什么也不做。
    • broadcast 唤醒所有在等待的线程,如果没有线程在等待,什么也不做。

    以上的方法在调用前必须已经加锁(lock)

    NSCondition 是条件,条件是我们自己决定的。和 NSLock@synchronized 等是不同的是,NSCondition 可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。代码示例:

    NSCondition *condition =[[NSCondition alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [condition lock];
        NSLog(@"线程1加锁");
        while ([self.testArray count] == 0) {
            NSLog(@"waiting...");
            [condition wait];
        }
        [self.testArray removeObjectAtIndex:0];
        NSLog(@"delete one object");
        NSLog(@"线程1退出");
        [condition unlock];
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [condition lock];
        NSLog(@"线程2加锁");
        self.testArray = [NSMutableArray array];
        [self.testArray addObject:[[NSObject alloc] init]];
        NSLog(@"add one object");
        [condition signal];
        NSLog(@"线程2退出");
        [condition unlock];
    });
    

    condition 进入到判断条件中,当 self.testArray count == 0 时,condition 会调用 wait,当前线程处于等待状态,其他线程开始访问 self.testArray,当对象创建完毕并加入 self.testArray 中时,cpu 会发出 signal 信号,处于等待的线程就会被唤醒,开始执行 [self.testArray removeObjectAtIndex:0];

    打印结果如下:

    NSConditionLock

    NSConditionLock 为条件锁,只有 condition 参数与初始化时候的 condition 相等,才能进行加锁操作。而 unlockWithCondition: 并不是当 condition 符合条件时才解锁,而是解锁之后,修改 condition 的值。提供的方法如下:

    - (instancetype)initWithCondition:(NSInteger)condition; //初始化对象。有一个整形的conditon参数,表示条件
    - (void)lockWhenCondition:(NSInteger)condition; //进程会一直阻塞,一直到满足conditon并完成加锁
    - (BOOL)tryLock;
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition; //解锁并重新设定condition
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    

    我们来看个示例:

    NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:3];
    
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [lock lockWhenCondition:1];
       NSLog(@"线程1开始执行");
       [lock unlockWithCondition:0];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [lock lockWhenCondition:2];
       NSLog(@"线程2开始执行");
       [lock unlockWithCondition:1];
    });
    
    //线程3
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [lock lock];
       NSLog(@"线程3开始执行");
       [lock unlock];
    });
    
    //线程4
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [lock lockWhenCondition:3];
       NSLog(@"线程4开始执行");
       [lock unlockWithCondition:2];
    });
    
    //线程5
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       [lock lock];
       NSLog(@"线程5开始执行");
       [lock unlock];
    });
    

    分析:

    • 线程1,2调用 [NSConditionLock lockWhenCondition:],此时因为不满足当前条件,所以会进入等待状态。

    • 线程3,5调用 [NSConditionLock lock:],不需要比对条件值,按照 cpu 执行顺序执行,

    • 线程4执行 [NSConditionLock lockWhenCondition:],因为满足条件值,所以线程4会按照 cpu 执行顺序执行。

    • 线程4打印完成后会调用 [NSConditionLock unlockWithCondition:],这个时候将条件设置为2,并发送 boradcast,此时线程2接收到当前的信号,唤醒执行并打印;之后会执行线程1。

    [NSConditionLock lockWhenCondition:] 这里会根据传入的 condition 值和 value 值进行对比,如果不相等,这里就会阻塞。而 [NSConditionLock unlockWithCondition:] 会先更改当前的 value 值,然后调用 boradcast,唤醒当前的线程。综上所述,上面的打印结果不是一定的,421 的顺序是一定的,而 3,5 是在任意位置(即只要是按照421的结果顺序都是正确的)

    NSCondition & NSConditionLock 比较

    相同点:

    • 都是互斥锁

    • 通过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的

    不同点:

    • NSCondition 是基于对 pthread_mutex 的封装,而 NSConditionLock 是对 NSCondition 做了一层封装

    • NSCondition 需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程,NSConditionLock 只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程

    @synchronized

    是一个 OC 层面的互斥锁,主要是通过牺牲性能换来语法上的简洁与可读。(性能较差不推荐使用)

    @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁的唯一标识。这是通过一个哈希表来记录表示,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值在数组中得到对应的互斥锁。示例如下:

    //总票数
    _tickets = 5;
    
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleTickets];
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleTickets];
    });
    
    - (void)saleTickets {
        while (1) {
            @synchronized (self) {
                [NSThread sleepForTimeInterval:1];
                if (_tickets > 0) {
                    _tickets--;
                    NSLog(@"剩余票数:%ld, Thread:%@", _tickets, [NSThread currentThread]);
                }
                else {
                    NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
                    break;
                }
            }
        }
    }
    

    注意点:

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

    dispatch_semaphore

    dispatch_semaphoreGCD 使用信号量控制并发。

    信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。

    在日常开发中利用 GCD 的信号量机制来处理一些日常功能的时候,主要会用到的方法有三个:

    //创建信号量,会根据传入的参数创建对应数目的信号量
    dispatch_semaphore_create(intptr_t value);;
    
    //等待信号量,减少信号量计数
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
    
    // 发送信号量,增加信号量计数
    dispatch_semaphore_signal(dispatch_semaphore_t dsema);
    
    • dispatch_semaphore_create 创建信号量,并且创建的时候需要指定信号量的大小

    • dispatch_semaphore_wait 等待信号量,如果信号量为0,那么该函数就会一直等待(不返回,阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。

    • dispatch_semaphore_signal 发送信号量。该函数会对信号量的值进行加1操作

    等待信号量和发送信号量的函数是成对出现的。下面来看个经典的示例(异步函数+并发队列实现同步操作):

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    for (int i = 0; i < 100; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"任务%d:%@", i + 1, [NSThread currentThread]);
            // 发送信号量
            dispatch_semaphore_signal(semaphore);
        });
    
        // 等待信号量
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务1000:%@",[NSThread currentThread]);
    });
    

    打印结果太长,截取一部分如下:

    可以看到:虽然任务是一个接一个被同步(说同步并不准确)执行的,但因为是在并发队列,并不是所有的任务都是在同一个线程执行的(所以说同步并不准确)。有别于异步函数+串行队列的方式(异步函数+ 串行队列的方式中,所有的任务都是在同一个新线程被串行执行的)。

    同步和异步决定了是否开启新线程(或者说是否具有开启新线程的能力),串行和并发决定了任务的执行方式——串行执行还是并发执行(或者说开启多少条新线程)

    pthread

    pthread,可以创建互斥锁、递归锁、读写锁、once等锁

    互斥锁

    不会忙等,而是阻塞线程并睡眠,需要进行上下文切换。

    __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, PTHREAD_MUTEX_NORMAL);
    /**
     PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
     PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
     PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
     PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
     */
    
    //线程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程1加锁");
        pthread_mutex_lock(&mutex);
        sleep(2);
        NSLog(@"线程1");
        pthread_mutex_unlock(&mutex);
        NSLog(@"线程1解锁");
    });
    
    //线程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"线程2加锁");
        pthread_mutex_lock(&mutex);
        sleep(2);
        NSLog(@"线程2");
        pthread_mutex_unlock(&mutex);
        NSLog(@"线程2解锁");
    });
    
    递归锁
    __block pthread_mutex_t recursiveMutex;
    pthread_mutexattr_t recursiveMutexattr;
    
    pthread_mutexattr_init(&recursiveMutexattr);
    pthread_mutexattr_settype(&recursiveMutexattr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&recursiveMutex, &recursiveMutexattr);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveBlock)(int);
    
        RecursiveBlock = ^(int value) {
            NSLog(@"线程加锁");
            pthread_mutex_lock(&recursiveMutex);
            if (value > 0) {
                NSLog(@"value: %d",value);
                RecursiveBlock(--value);
            }
            NSLog(@"线程解锁");
            pthread_mutex_unlock(&recursiveMutex);
        };
    
        RecursiveBlock(3);
    });
    
    读写锁
    typedef void(^ReadWriteBlock)(NSString *str);
    typedef void(^VoidBlock)(void);
    
    __block pthread_rwlock_t rwlock;
    pthread_rwlock_init(&rwlock, NULL);
    __block NSMutableArray *arrayM = [NSMutableArray array];
    
    ReadWriteBlock writeBlock = ^ (NSString *str) {
        NSLog(@"开启写操作");
        pthread_rwlock_wrlock(&rwlock);
        [arrayM addObject:str];
        sleep(2);
        pthread_rwlock_unlock(&rwlock);
    };
    
    VoidBlock readBlock = ^  {
        NSLog(@"开启读操作");
        pthread_rwlock_rdlock(&rwlock);
        sleep(1);
        NSLog(@"读取数据:%@",arrayM);
        pthread_rwlock_unlock(&rwlock);
    };
    
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            writeBlock([NSString stringWithFormat:@"%d",I]);
        });
    }
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            readBlock();
        });
    }
    

    dispatch_barrier_async / dispatch_barrier_sync

    现在有一个需求:任务1,2,3 均执行完毕执行任务 0,然后执行任务4,5,6,我们通过 GCD 的 barrier 方法来实现,如下:

    - (void)dispatch_barrierTest {
        dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lc.brrier", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(concurrentQueue, ^{
            NSLog(@"任务1 -- %@", [NSThread currentThread]);
        });
        
        dispatch_async(concurrentQueue, ^{
            NSLog(@"任务2 -- %@", [NSThread currentThread]);
        });
        
        dispatch_async(concurrentQueue, ^{
            NSLog(@"任务3 -- %@", [NSThread currentThread]);
        });
        
        dispatch_barrier_sync(concurrentQueue, ^{
            NSLog(@"任务0 -- %@", [NSThread currentThread]);
            sleep(5); //默认耗时
        });
        
        NSLog(@"dispatch_barrier 测试");
        
        dispatch_async(concurrentQueue, ^{
            NSLog(@"任务4 -- %@", [NSThread currentThread]);
        });
        dispatch_async(concurrentQueue, ^{
            NSLog(@"任务5 -- %@", [NSThread currentThread]);
        });
        dispatch_async(concurrentQueue, ^{
            NSLog(@"任务6 -- %@", [NSThread currentThread]);
        });
    }
    

    运行,输出结果如下:

    可以看到,任务0执行是在任务1,2,3 都执行完之后才会执行,而任务4,5,6是在任务0执行后才会执行(其中1,2,3 是不分先后顺序,同样的4,5,6也不分先后顺序)

    dispatch_barrier_async 和 dispatch_barrier_sync 的区别

    相同点

    • 等待前面的任务都执行完毕才会执行当前的任务

    • 当前任务执行完毕才会执行后面的任务

    不同点

    • dispatch_barrier_async 将当前任务添加到队列之后,会将后续的任务也添加到队列中,但是后面的任务只能等待当前任务执行完毕,才会执行后面的任务

    • dispatch_barrier_sync 将当前任务添加到队列之后,等待当前任务执行完毕,才会将后续的任务添加到队列,然后执行任务

    将上述代码中的 dispatch_barrier_sync 换成 dispatch_barrier_async 后,输出结果为:

    拓展

    property - atomic & nonatomic

    atomic 修饰的对象,系统会保证在其自动生成的 getter/setter 方法中的操作是完整的,不受其他线程的影响。

    atomic
    • 默认修饰符
    • 会保证CPU能在别的线程访问这个属性之前先执行完当前操作
    • 读写速度慢
    • 线程不安全 - 如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
    nonatomic
    • 手动写
    • 速度更快
    • 线程不安全
    • 如果两个线程同时访问会出现不可预料的结果

    单例实现

    单例:该类在程序运行期间有且仅有一个实例

    使用 GCD 来实现
    - (id)shareInstance {
        static id shareInstance;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            if (!shareInstance) {
                shareInstance = [[NSObject alloc] init];
            }
        });
        return shareInstance;
    }
    
    通过 pthread 来实现
    - (void)lock {
        pthread_once_t once = PTHREAD_ONCE_INIT;
        pthread_once(&once, onceFunc);
    }
    
    void onceFunc() {
        static id shareInstance;
        shareInstance = [[NSObject alloc] init];
    }
    

    相关文章

      网友评论

          本文标题:iOS 开发常见的几种锁

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