iOS中的锁的介绍及使用

在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那?

线程安全是什么?

当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。

比如写文件和读文件,当一个线程在写文件的时候,理论上来说,如果这个时候另一个线程来直接读取的话,那么得到的结果可能是你无法预料的。

怎么来保证线程安全?

通常我们使用锁的机制来保证线程安全,即确保同一时刻只有同一个线程来对同一个数据源进行访问。
YY大神 的 不再安全的 OSSpinLock 这边博客中列出了各种锁以及性能比较:

性能对比

这里性能比较的只是加锁立马解锁的时间消耗,并没有计算竞争时候的时间消耗。

锁的介绍及简单使用

1.@synchronized

@synchronized是 iOS 中最常见的锁,用法很简单:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self synchronized];
}

- (void)synchronized {
    NSObject * obj = [NSObject new];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        @synchronized(obj){
            NSLog(@"线程1开始");
            sleep(3);
            NSLog(@"线程1结束");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        @synchronized(obj){
            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

从上面的控制台输出时间可以看出来,在线程 1 内容全部输出之后,才输出了线程 2 的内容,“线程1结束”与“线程2”都是在“线程1开始”3 秒后输出的。

  1. @synchronized(obj) 指令使用的 obj 为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程 2 中的 @synchronized(obj) 改为 @synchronized(self) ,那么线程 2 就不会被阻塞;

  2. @synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制;

  3. @synchronized 是对 mutex递归锁(下文讲述) 的封装:

    • 源码查看:objc4中的objc-sync.mm文件
    • @synchronized(obj) 内部会生成 obj 对应的 pthread_mutex 递归锁,然后进行加锁、解锁操作。
      根据 obj 地址生成一个索引值,然后创建一个 pthread_mutex 递归锁 作为 value,一起保存到哈希表中;
  4. @sychronized(cjobj){} 内部 cjobj 被释放或被设为 nil 不会影响锁的功能,但如果 cjobj 一开始就是 nil,那就会丢失了锁的功能了。

2.NSLock

先看看iOS中的 NSLock.h 文件,从代码中可以看出,这里定义了几个类:NSLock、NSConditionLock、NSRecursiveLock、NSCondition,然后有一个 NSLocking 协议:

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

虽然 NSLock、NSConditionLock、NSRecursiveLock、NSCondition 都遵循的了 NSLocking 协议,但是它们并不相同。

2.1 NSLock

NSLock 是对 mutex普通锁(PTHREAD_MUTEX_NORMAL) 的封装,它实现了最基本的互斥锁,遵循了 NSLocking 协议,通过 lock 和 unlock 来进行锁定和解锁。

源码内容:

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

用法:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self nslock];
}

- (void)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解锁成功");
    });
}

控制台输出:
2017-10-19 15:03:58.868708+0800 Thread-Lock[39059:846493] 线程1加锁成功
2017-10-19 15:04:00.872714+0800 Thread-Lock[39059:846493] 线程1解锁成功
2017-10-19 15:04:00.872722+0800 Thread-Lock[39059:846492] 线程2加锁成功
2017-10-19 15:04:00.873000+0800 Thread-Lock[39059:846492] 线程2解锁成功


- (void)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), ^{
        if ([cjlock tryLock]) {
            NSLog(@"线程3加锁成功");
            [cjlock unlock];
            NSLog(@"线程3解锁成功");
        }else {
            NSLog(@"线程3加锁失败");
        }
    });
}

控制台输出:
2017-10-19 15:05:38.627767+0800 Thread-Lock[39118:849171] 线程1加锁成功
2017-10-19 15:05:38.627767+0800 Thread-Lock[39118:849169] 线程3加锁失败
2017-10-19 15:05:40.629969+0800 Thread-Lock[39118:849171] 线程1解锁成功

- (void)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(3);
        if ([cjlock tryLock]) {
            NSLog(@"线程4加锁成功");
            [cjlock unlock];
            NSLog(@"线程4解锁成功");
        }else {
            NSLog(@"线程4加锁失败");
        }
    });
}

控制台输出:
2017-10-19 15:07:14.872279+0800 Thread-Lock[39166:851060] 线程1加锁成功
2017-10-19 15:07:16.876108+0800 Thread-Lock[39166:851060] 线程1解锁成功
2017-10-19 15:07:17.876208+0800 Thread-Lock[39166:851052] 线程4加锁成功
2017-10-19 15:07:17.876527+0800 Thread-Lock[39166:851052] 线程4解锁成功

- (void)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), ^{
        if ([cjlock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
            NSLog(@"线程5加锁成功");
            [cjlock unlock];
            NSLog(@"线程5解锁成功");
        }else {
            NSLog(@"线程5加锁失败");
        }
    });
}

控制台输出:
2017-10-19 15:08:39.705131+0800 Thread-Lock[39204:852782] 线程1加锁成功
2017-10-19 15:08:41.708717+0800 Thread-Lock[39204:852782] 线程1解锁成功
2017-10-19 15:08:41.708717+0800 Thread-Lock[39204:852784] 线程5加锁成功
2017-10-19 15:08:41.708935+0800 Thread-Lock[39204:852784] 线程5解锁成功

注意:lock与unlock操作必须在同一线程,否则结果不确定甚至会引起死锁

由以上内容总结:

  • 除 lock 和 unlock 方法外,NSLock 还提供了 tryLock 和 lockBeforeDate:两个方法。
  • 由上面的结果可以看到 tryLock 并不会阻塞线程,[cjlock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码。
  • 这里顺便提一下 trylock 和 lock 使用场景:当前线程锁失败,也可以继续其它任务,用 trylock 合适;当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock。以下的锁都是这样。
  • lockBeforeDate: 方法会在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回 YES。
  • 由于是互斥锁,当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。但是如果连续锁定两次,则会造成死锁问题。

2.2 NSRecursiveLock

NSRecursiveLock 是对 mutex递归锁(PTHREAD_MUTEX_RECURSIVE) 的封装,递归锁顾名思义,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。NSRecursiveLock 会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。

源码内容:

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

用法:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self nsrecursivelock];
}

- (void)nsrecursivelock{
    NSRecursiveLock * cjlock = [[NSRecursiveLock alloc] init];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^RecursiveBlock)(int);
        RecursiveBlock = ^(int value) {
            [cjlock lock];
            NSLog(@"%d加锁成功",value);
            if (value > 0) {
                NSLog(@"value:%d", value);
                RecursiveBlock(value - 1);
            }
            [cjlock unlock];
            NSLog(@"%d解锁成功",value);
        };
        RecursiveBlock(3);
    });
}

控制台输出:
2017-10-19 16:15:40.584213+0800 Thread-Lock[39579:894111] 3加锁成功
2017-10-19 16:15:40.584387+0800 Thread-Lock[39579:894111] value:3
2017-10-19 16:15:40.584552+0800 Thread-Lock[39579:894111] 2加锁成功
2017-10-19 16:15:40.584635+0800 Thread-Lock[39579:894111] value:2
2017-10-19 16:15:40.584810+0800 Thread-Lock[39579:894111] 1加锁成功
2017-10-19 16:15:40.585267+0800 Thread-Lock[39579:894111] value:1
2017-10-19 16:15:40.585714+0800 Thread-Lock[39579:894111] 0加锁成功
2017-10-19 16:15:40.585906+0800 Thread-Lock[39579:894111] 0解锁成功
2017-10-19 16:15:40.586138+0800 Thread-Lock[39579:894111] 1解锁成功
2017-10-19 16:15:40.586217+0800 Thread-Lock[39579:894111] 2解锁成功
2017-10-19 16:15:40.586314+0800 Thread-Lock[39579:894111] 3解锁成功

由以上内容总结:

  • 如果用 NSLock 的话,cjlock 先锁上了,但未执行解锁的时候,就会进入递归的下一层,而再次请求上锁,阻塞了该线程,线程被阻塞了,自然后面的解锁代码不会执行,而形成了死锁。而 NSRecursiveLock 递归锁就是为了解决这个问题。

2.3、NSCondition
NSCondition 是对 mutex普通锁(PTHREAD_MUTEX_NORMAL) 和 pthread_cond_t 的封装,它是一种特殊类型的锁,通过它可以实现不同线程的调度。一个线程被某一个条件所阻塞,直到另一个线程满足该条件从而发送信号给该线程使得该线程可以正确的执行。比如说,你可以开启一个线程下载图片,一个线程处理图片。这样的话,需要处理图片的线程由于没有图片会阻塞,当下载线程下载完成之后,则满足了需要处理图片的线程的需求,这样可以给定一个信号,让处理图片的线程恢复运行。

  • NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,如果进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。
  • NSCondition 可以手动控制线程的挂起与唤醒,可以利用这个特性设置依赖。

源码内容:

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait; //挂起线程
- (BOOL)waitUntilDate:(NSDate *)limit; //什么时候挂起线程
- (void)signal; // 唤醒一条挂起线程
- (void)broadcast; //唤醒所有挂起线程

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

用法:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self nscondition];
}

- (void)nscondition {
    NSCondition * cjcondition = [NSCondition new];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [cjcondition lock];
        NSLog(@"线程1线程加锁");
        [cjcondition wait];
        NSLog(@"线程1线程唤醒");
        [cjcondition unlock];
        NSLog(@"线程1线程解锁");
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [cjcondition lock];
        NSLog(@"线程2线程加锁");
        if ([cjcondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
            NSLog(@"线程2线程唤醒");
            [cjcondition unlock];
            NSLog(@"线程2线程解锁");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [cjcondition signal];
    });
}

控制台输出:
2017-10-19 17:15:48.410316+0800 Thread-Lock[40011:943638] 线程1线程加锁
2017-10-19 17:15:48.410757+0800 Thread-Lock[40011:943640] 线程2线程加锁
2017-10-19 17:15:50.414288+0800 Thread-Lock[40011:943638] 线程1线程唤醒
2017-10-19 17:15:50.414454+0800 Thread-Lock[40011:943638] 线程1线程解锁

//如果 [cjcondition signal]; 改成 [cjcondition broadcast];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);
        [cjcondition broadcast];
    });

控制台输出:
2017-10-19 17:18:08.054109+0800 Thread-Lock[40056:946099] 线程1线程加锁
2017-10-19 17:18:08.054304+0800 Thread-Lock[40056:946096] 线程2线程加锁
2017-10-19 17:18:10.056071+0800 Thread-Lock[40056:946099] 线程1线程唤醒
2017-10-19 17:18:10.056231+0800 Thread-Lock[40056:946099] 线程1线程解锁
2017-10-19 17:18:10.056244+0800 Thread-Lock[40056:946096] 线程2线程唤醒
2017-10-19 17:18:10.056445+0800 Thread-Lock[40056:946096] 线程2线程解锁

由以上内容总结:

  • 在加上锁之后,调用条件对象的 wait 或 waitUntilDate: 方法来阻塞线程,直到条件对象发出唤醒信号或者超时之后,再进行之后的操作。
  • signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。

2.4 NSConditionLock
NSConditionLock 是对 NSCondition 的进一步封装,它的对象所定义的互斥锁可以在使得在某个条件下进行锁定和解锁,它和 NSLock 类似,都遵循 NSLocking 协议,方法都类似,只是多了一个 condition 属性,以及每个操作都多了一个关于 condition 属性的方法,例如 tryLock、tryLockWhenCondition:,所以 NSConditionLock 可以称为条件锁。

  • 只有 condition 参数与初始化时候的 condition 相等,lock 才能正确进行加锁操作。
  • unlockWithCondition: 并不是当 condition 符合条件时才解锁,而是解锁之后,修改 condition 的值。

源码内容:

@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 API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

用法:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self nsconditionlock];
}

- (void)nsconditionlock {
    NSConditionLock * cjlock = [[NSConditionLock alloc] initWithCondition:0];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [cjlock lock];
        NSLog(@"线程1加锁成功");
        sleep(1);
        [cjlock unlock];
        NSLog(@"线程1解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        [cjlock lockWhenCondition:1];
        NSLog(@"线程2加锁成功");
        [cjlock unlock];
        NSLog(@"线程2解锁成功");
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(2);
        if ([cjlock tryLockWhenCondition:0]) {
            NSLog(@"线程3加锁成功");
            sleep(2);
            [cjlock unlockWithCondition:2];
            NSLog(@"线程3解锁成功");
        } else {
            NSLog(@"线程3尝试加锁失败");
        }
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        if ([cjlock lockWhenCondition:2 beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]) {
            NSLog(@"线程4加锁成功");
            [cjlock unlockWithCondition:1];
            NSLog(@"线程4解锁成功");
        } else {
            NSLog(@"线程4尝试加锁失败");
        }
    });
}

控制台输出:
2017-10-19 15:09:44.010992+0800 Thread-Lock[39230:853946] 线程1加锁成功
2017-10-19 15:09:45.012045+0800 Thread-Lock[39230:853946] 线程1解锁成功
2017-10-19 15:09:46.012692+0800 Thread-Lock[39230:853947] 线程3加锁成功
2017-10-19 15:09:48.016536+0800 Thread-Lock[39230:853947] 线程3解锁成功
2017-10-19 15:09:48.016564+0800 Thread-Lock[39230:853944] 线程4加锁成功
2017-10-19 15:09:48.017039+0800 Thread-Lock[39230:853944] 线程4解锁成功
2017-10-19 15:09:48.017040+0800 Thread-Lock[39230:853945] 线程2加锁成功
2017-10-19 15:09:48.017215+0800 Thread-Lock[39230:853945] 线程2解锁成功

由以上内容总结:

  • 在线程 1 解锁成功之后,线程 2 并没有加锁成功,而是继续等了 1 秒之后线程 3 加锁成功,这是因为线程 2 的加锁条件不满足,初始化时候的 condition 参数为 0,而线程 2
    加锁条件是 condition 为 1,所以线程 2 加锁失败。
  • lockWhenCondition 与 lock 方法类似,加锁失败会阻塞线程,所以线程 2 会被阻塞着。
  • tryLockWhenCondition: 方法就算条件不满足,也会返回 NO,不会阻塞当前线程。
  • lockWhenCondition:beforeDate:方法会在约定的时间内一直等待 condition 变为 2,并阻塞当前线程,直到超时后返回 NO。
  • 锁定和解锁的调用可以随意组合,也就是说 lock、lockWhenCondition:与unlock、unlockWithCondition: 是可以按照自己的需求随意组合的。

3.dispatch_semaphore

dispatch_semaphore 使用信号量机制实现锁,等待信号和发送信号。

  • dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。
  • dispatch_semaphore 的机制就是当有多个线程进行访问的时候,只要有一个获得了信号,其他线程的就必须等待该信号释放。

常用相关API:

dispatch_semaphore_create(long value);
dispatch_semaphore_wait(dispatch_semaphore_t  _Nonnull dsema, dispatch_time_t timeout);
dispatch_semaphore_signal(dispatch_semaphore_t  _Nonnull dsema);

用法:

- (void)viewDidLoad {
    [super viewDidLoad];

    [self dispatch_semaphore];
}

- (void)dispatch_semaphore {
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 6 * NSEC_PER_SEC);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(semaphore, overTime);
        NSLog(@"线程1开始");
        sleep(5);
        NSLog(@"线程1结束");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);
        dispatch_semaphore_wait(semaphore, overTime);
        NSLog(@"线程2开始");
        dispatch_semaphore_signal(semaphore);
    });
}

控制台输出:
2017-10-19 18:30:37.672490+0800 Thread-Lock[40569:993613] 线程1开始
2017-10-19 18:30:42.673845+0800 Thread-Lock[40569:993613] 线程1结束
2017-10-19 18:30:42.674165+0800 Thread-Lock[40569:993612] 线程2开始

//如果 overTime 改成 3 秒
控制台输出:
2017-10-19 18:32:32.078186+0800 Thread-Lock[40634:995921] 线程1开始
2017-10-19 18:32:35.082943+0800 Thread-Lock[40634:995920] 线程2开始
2017-10-19 18:32:37.083115+0800 Thread-Lock[40634:995921] 线程1结束

由以上内容总结:

  • dispatch_semaphore 和 NSCondition 类似,都是一种基于信号的同步方式,但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。
  • dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 1。注意,这里的传入的参数必须大于或等于 0,否则 dispatch_semaphore_create 会返回 NULL。
  • dispatch_semaphore_wait(semaphore, overTime); 方法会判断 semaphore 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。
  • dispatch_semaphore_signal(semaphore); 发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)。
  • 一个 dispatch_semaphore_wait(semaphore, overTime); 方法会去对应一个 dispatch_semaphore_signal(semaphore); 看起来像 NSLock 的 lock 和 unlock,其实可以这样理解,区别只在于有信号量这个参数,lock unlock 只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore 的信号量初始值为 x ,则可以有 x 个线程同时访问被保护的临界区。

4.pthread_mutex 与 pthread_mutex(recursive)

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,POSIX 互斥锁是一种超级易用的互斥锁,使用的时候:

  • 只需要使用 pthread_mutex_init 初始化一个 pthread_mutex_t,
  • pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定 ,
  • pthread_mutex_unlock 来解锁,
  • 当使用完成后,记得调用 pthread_mutex_destroy 来销毁锁。

常用相关API:

pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable);
pthread_mutex_lock(pthread_mutex_t * _Nonnull);
pthread_mutex_trylock(pthread_mutex_t * _Nonnull);
pthread_mutex_unlock(pthread_mutex_t * _Nonnull);
pthread_mutex_destroy(pthread_mutex_t * _Nonnull);

用法:

//pthread_mutex

- (void)viewDidLoad {
    [super viewDidLoad];

    [self pthread_mutex];
}

- (void)pthread_mutex {
    __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);
    });
}

控制台输出:
2017-10-23 14:50:29.842180+0800 Thread-Lock[74478:1647362] 线程1开始
2017-10-23 14:50:32.846786+0800 Thread-Lock[74478:1647362] 线程1结束
2017-10-23 14:50:32.847001+0800 Thread-Lock[74478:1647359] 线程2


//pthread_mutex(recursive)

- (void)viewDidLoad {
    [super viewDidLoad];

    [self pthread_mutex_recursive];
}

- (void)pthread_mutex_recursive {
    __block pthread_mutex_t cjlock;
    
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&cjlock, &attr);
    pthread_mutexattr_destroy(&attr);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        static void (^RecursiveBlock)(int);
        
        RecursiveBlock = ^(int value) {
            pthread_mutex_lock(&cjlock);
            NSLog(@"%d加锁成功",value);
            if (value > 0) {
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveBlock(value - 1);
            }
            NSLog(@"%d解锁成功",value);
            pthread_mutex_unlock(&cjlock);
        };
        RecursiveBlock(3);
    });
}

//控制台输出:
2017-10-23 15:31:51.599693+0800 Thread-Lock[74723:1668089] 3加锁成功
2017-10-23 15:31:51.599912+0800 Thread-Lock[74723:1668089] value = 3
2017-10-23 15:31:52.602002+0800 Thread-Lock[74723:1668089] 2加锁成功
2017-10-23 15:31:52.602317+0800 Thread-Lock[74723:1668089] value = 2
2017-10-23 15:31:53.604669+0800 Thread-Lock[74723:1668089] 1加锁成功
2017-10-23 15:31:53.604957+0800 Thread-Lock[74723:1668089] value = 1
2017-10-23 15:31:54.607778+0800 Thread-Lock[74723:1668089] 0加锁成功
2017-10-23 15:31:54.608109+0800 Thread-Lock[74723:1668089] 0解锁成功
2017-10-23 15:31:54.608391+0800 Thread-Lock[74723:1668089] 1解锁成功
2017-10-23 15:31:54.608622+0800 Thread-Lock[74723:1668089] 2解锁成功
2017-10-23 15:31:54.608945+0800 Thread-Lock[74723:1668089] 3解锁成功

由以上内容总结:

  • 它的用法和 NSLock 的 lock unlock 用法一致,而它也有一个 pthread_mutex_trylock 方法,pthread_mutex_trylock 和 tryLock 的区别在于,tryLock 返回的是 YES 和 NO,pthread_mutex_trylock 加锁成功返回的是 0,失败返回的是错误提示码。
  • pthread_mutex(recursive) 是递归锁,如果使用 pthread_mutex_init(&theLock, NULL); 初始化锁的话,上面的代码的第二部分会出现死锁现象,使用递归锁就可以避免这种现象。

5. OSSpinLock

OSSpinLock 叫做” 自旋锁 ”,等待锁的线程会处于 忙等(busy-wait)状态 ,相当于一直执行 while(锁还没打开),自己旋转着等着锁释放,一直占用着CPU资源,所以自旋锁比较适用于线程等待锁的时间很短的情况;对比互斥锁,当一个线程获得这个锁之后,等待此锁的线程将会进入休眠状态,直到该锁被释放,不占用 CPU 资源。

  • OSSpinLock 目前已经不再安全,可能会出现优先级反转问题:
    • 例如:如果优先级较低线程先获取了锁,而等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程因为无法执行完自己的任务就无法释放锁;
  • iOS 10 之后替换成 os_unfair_lock;
  • 只有加锁,解锁,尝试加锁三个方法。

常用相关API:

typedef int32_t OSSpinLock;

// 加锁
void    OSSpinLockLock( volatile OSSpinLock *__lock );
// 尝试加锁
bool    OSSpinLockTry( volatile OSSpinLock *__lock );
// 解锁
void    OSSpinLockUnlock( volatile OSSpinLock *__lock );

用法:

#import <libkern/OSAtomic.h>

- (void)viewDidLoad {
    [super viewDidLoad];

    [self osspinlock];
}

- (void)osspinlock {
    __block OSSpinLock theLock = OS_SPINLOCK_INIT;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        NSLog(@"线程1开始");
        sleep(3);
        NSLog(@"线程1结束");
        OSSpinLockUnlock(&theLock);
        
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        OSSpinLockLock(&theLock);
        sleep(1);
        NSLog(@"线程2");
        OSSpinLockUnlock(&theLock);
        
    });
}

//控制台输出:
2017-10-23 16:02:48.865501+0800 Thread-Lock[75025:1684487] 线程1开始
2017-10-23 16:02:51.868736+0800 Thread-Lock[75025:1684487] 线程1结束
2017-10-23 16:02:52.922911+0800 Thread-Lock[75025:1684486] 线程2

YY大神 @ibireme 的文章也有说这个自旋锁存在优先级反转问题,具体文章可以戳 不再安全的 OSSpinLock,而 OSSpinLock 在iOS 10.0中被 <os/lock.h> 中的 os_unfair_lock 取代。

6.os_unfair_lock

自旋锁已经不再安全,然后苹果又整出来个 os_unfair_lock,这个锁解决了优先级反转问题。

常用相关API:

// 初始化
os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
// 加锁
os_unfair_lock_lock(unfairLock);
// 尝试加锁
BOOL b = os_unfair_lock_trylock(unfairLock);
// 解锁
os_unfair_lock_unlock(unfairLock);

os_unfair_lock 用法和 OSSpinLock 基本一直,就不一一列出了。

总结

应当针对不同的操作使用不同的锁,而不能一概而论哪种锁的加锁解锁速度快。

  • 其实每一种锁基本上都是加锁、等待、解锁的步骤,理解了这三个步骤就可以帮你快速的学会各种锁的用法。
  • @synchronized 的效率最低,不过它的确用起来最方便,所以如果没什么性能瓶颈的话,可以选择使用 @synchronized。
  • 当性能要求较高时候,可以使用 pthread_mutex 或者 dispath_semaphore,由于 OSSpinLock 不能很好的保证线程安全,而在只有在 iOS10 中才有 os_unfair_lock ,所以,前两个是比较好的选择。既可以保证速度,又可以保证线程安全。

如果想了解每个锁的实现原理可以看一下这个,非常赞:
深入理解 iOS 开发中的锁

参考:
iOS中保证线程安全的几种方式与性能对比
不再安全的 OSSpinLock
iOS 中的各种锁
关于@synchronized,这儿比你想知道的还要多

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容