美文网首页iOS
【iOS】 常见的各种锁Lock 🔐

【iOS】 常见的各种锁Lock 🔐

作者: emmet7life | 来源:发表于2018-08-22 21:15 被阅读36次
    1. 性能对比

    (来自 ibireme 的 [不再安全的 OSSpinLock][] 一文)

    性能对比
    2. 特点分析

    2.1. @synchronized(obj)

    可能是我们最常用的方式,但是它的性能是最差的,😭,obj是该锁的唯一标识,只有当标识相同时,才能满足互斥需求,优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

    2.2 dispatch_semaphore

    dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。

    dispatch_semaphor 和 NSCondition 类似,都是一种基于信号的同步方式,但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 ispatch_semaphore_t 类型的信号量。

    dispatch_semaphore_create(1)方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 1。注意,这里的传入的参数必须大于或等于 0,否则 dispatch_semaphore_create 会返回 NULL。

    dispatch_semaphore_wait(signal, timeout)方法会判断 signal 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 timeout 时限到了,也会执行后续任务。

    dispatch_semaphore_signal(signal)发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)。

    从上面的实例代码可以看到,一个dispatch_semaphore_wait(signal, timeout)方法会去对应一个 dispatch_semaphore_signal(signal)看起来像 NSLock 的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore 的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区。

    2.3 NSLock

    NSLock是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,除lock和unlock方法外,NSLock还提供了tryLock和lockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。

    @protocol NSLocking
    
    - (void)lock;
    - (void)unlock;
    
    @end
    
    @interface NSLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    
    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
    
    @end
    

    2.4 NSRecursiveLock

    NSRecursiveLock是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

    //NSLock *lock = [[NSLock alloc] init];
    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(1);
                RecursiveMethod(value - 1);
            }
            [lock unlock];
        };
            
        RecursiveMethod(5);
    });
    

    这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。

    在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

    2.5 NSConditionLock

    当我们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁。

    @interface NSConditionLock : NSObject <NSLocking> {
    @private
        void *_priv;
    }
    
    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
    
    @property (readonly) NSInteger condition;
    - (void)lockWhenCondition:(NSInteger)condition;
    - (BOOL)tryLock;
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
    
    @end
    

    lock -> 表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁。

    lockWhenCondition:A条件 -> 表示如果没有其他线程获得该锁,但是该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。

    unlockWithCondition:A条件 -> 表示释放锁,同时把内部的condition设置为A条件。

    例子解析:

    //主线程中
    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的代码后执行
        if ([lock tryLockWhenCondition:2]) {
            NSLog(@"线程4");
            [lock unlockWithCondition:1];    
            NSLog(@"线程4解锁成功");
        } else {
            NSLog(@"线程4尝试加锁失败");
        }
    });
    
    2016-08-19 13:51:15.353 ThreadLockControlDemo[1614:110697] 线程2
    2016-08-19 13:51:15.354 ThreadLockControlDemo[1614:110697] 线程2解锁成功
    2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 线程3
    2016-08-19 13:51:16.353 ThreadLockControlDemo[1614:110689] 线程3解锁成功
    2016-08-19 13:51:17.354 ThreadLockControlDemo[1614:110884] 线程4
    2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 线程4解锁成功
    2016-08-19 13:51:17.355 ThreadLockControlDemo[1614:110884] 线程1
    
    • 上面代码先输出了 ”线程 2“,因为线程 1 的加锁条件不满足,初始化时候的 condition 参数为 0,而加锁条件是 condition 为 1,所以加锁失败。locakWhenCondition 与 lock 方法类似,加锁失败会阻塞线程,所以线程 1 会被阻塞着,而 tryLockWhenCondition 方法就算条件不满足,也会返回 NO,不会阻塞当前线程。

    • 回到上面的代码,线程 2 执行了 [lock unlockWithCondition:2]; 所以 Condition 被修改成了 2。

    • 而线程 3 的加锁条件是 Condition 为 2, 所以线程 3 才能加锁成功,线程 3 执行了 [lock unlock]; 解锁成功且不改变 Condition 值。

    • 线程 4 的条件也是 2,所以也加锁成功,解锁时将 Condition 改成 1。这个时候线程 1 终于可以加锁成功,解除了阻塞。

    • 从上面可以得出,NSConditionLock 还可以实现任务之间的依赖。

    2.6 NSCodition

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

    以下内容摘自官网:

    The semantics for using an NSCondition object are as follows:

    • Lock the condition object.

    • Test a boolean predicate. (This predicate is a boolean flag or other variable in your code that indicates whether it is safe to perform the task protected by the condition.)

    • If the boolean predicate is false, call the condition object’s wait() or wait(until:) method to block the thread. Upon returning from these methods, go to step 2 to retest your boolean predicate. (Continue waiting and retesting the predicate until it is true.)

    • If the boolean predicate is true, perform the task.

    • Optionally update any predicates (or signal any conditions) affected by your task.

    • When your task is done, unlock the condition object.

    The pseudocode for performing the preceding steps would therefore look something like the following:

    lock the condition
    while (!(boolean_predicate)) {
        wait on condition
    }
    do protected work
    (optionally, signal or broadcast the condition again or change a predicate value)
    unlock the condition
    

    以下是翻译(感谢 AidenRao):

    1. 锁定条件对象。

    2. 测试是否可以安全的履行接下来的任务。

    3. 如果布尔值是假的,调用条件对象的 wait 或 waitUntilDate: 方法来阻塞线程。 在从这些方法返回,则转到步骤 2 重新测试你的布尔值。 (继续等待信号和重新测试,直到可以安全的履行接下来的任务。waitUntilDate: 方法有个等待时间限制,指定的时间到了,则放回 NO,继续运行接下来的任务)

    4. 如果布尔值为真,执行接下来的任务。

    5. 当任务完成后,解锁条件对象。

    步骤 3 说的等待的信号,既线程 2 执行 [lock signal] 发送的信号。

    其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。如果没有等待的线程,这两个方法都没有作用。

    典型的生产者和消费者案例简析:

    • 消费者取得锁,取产品,如果没有,则wait,这时会释放锁,直到有线程唤醒它去消费产品;
    • 生产者制造产品,首先也要取得锁,然后生产,再发signal,这样可唤醒wait的消费者。

    2.7 pthread_mutex

    int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
    
    int pthread_mutex_lock(pthread_mutex_t *);// 加锁
    
    int pthread_mutex_trylock(pthread_mutex_t *);// 加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等
    
    int pthread_mutex_unlock(pthread_mutex_t *);// 释放锁 
    
    int pthread_mutex_destroy(pthread_mutex_t *);
    
    int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
      int * __restrict);
    
    int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
      int * __restrict);
    

    示例代码:

    static pthread_mutex_t theLock;
    
    - (void)example5 {
        pthread_mutex_init(&theLock, NULL);
        
        pthread_t thread;
        pthread_create(&thread, NULL, threadMethord1, NULL);
        
        pthread_t thread2;
        pthread_create(&thread2, NULL, threadMethord2, NULL);
    }
    
    void *threadMethord1() {
        pthread_mutex_lock(&theLock);
        printf("线程1\n");
        sleep(2);
        pthread_mutex_unlock(&theLock);
        printf("线程1解锁成功\n");
        return 0;
    }
    
    void *threadMethord2() {
        sleep(1);
        pthread_mutex_lock(&theLock);
        printf("线程2\n");
        pthread_mutex_unlock(&theLock);
        return 0;
    }
    
    线程1
    线程1解锁成功
    线程2
    

    const pthread_mutexattr_t * __restrict参数值类型:

    PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。

    PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。

    PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。

    PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。

    2.8 pthread_mutex(recursive)

    pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。

    __block pthread_mutex_t theLock;
    //pthread_mutex_init(&theLock, NULL);
    
    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);
    });
    

    如果使用pthread_mutex_init(&theLock, NULL);初始化锁的话,上面的代码会出现死锁现象。如果使用递归锁的形式,则没有问题。

    2.9 OSSpinLock

    OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。 不过最近YY大神在自己的博客不再安全的OSSpinLock中说明了OSSpinLock已经不再安全,请大家谨慎使用。

    参考文章:(可以按我列出的文章顺序挨个阅读一遍)

    1. iOS中保证线程安全的几种方式与性能对比 - 快速浏览
    2. iOS 常见知识点(三):Lock - 巩固复习
    3. bestswifter-深入理解iOS开发中的锁 - 深入理解
    4. 不再安全的OSSpinLock - 拓展阅读
    5. 关于 @synchronized,这儿比你想知道的还要多 - 深度好文

    相关文章

      网友评论

        本文标题:【iOS】 常见的各种锁Lock 🔐

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