浅谈iOS中的锁的介绍及使用

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

线程安全是什么?

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

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

怎么来保证线程安全?

通常我们使用锁的机制来保证线程安全,即确保同一时刻只有同一个线程来对同一个数据源进行访问。

YY大神 的 不再安全的 OSSpinLock 这边博客中列出了各种锁以及性能比较:

性能对比

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

锁的介绍及简单使用

1.@synchronized

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

- (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

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

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

@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 实现了最基本的互斥锁,遵循了 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操作必须在同一线程,否则结果不确定甚至会引起死锁

由以上内容总结:

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

2.2 NSRecursiveLock

NSRecursiveLock 是递归锁,顾名思义,可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。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 NSConditionLock

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

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

2.4、NSCondition

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

  1. NSCondition 的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入 waiting 状态,如果进入 waiting 状态,当其它线程中的该锁执行 signal 或者 broadcast 方法时,线程被唤醒,继续运行之后的方法。
  2. 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线程解锁

由以上内容总结:

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

3.dispatch_semaphore

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

  1. dispatch_semaphore 是 GCD 用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。
  2. 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结束

由以上内容总结:

  1. dispatch_semaphore 和 NSCondition 类似,都是一种基于信号的同步方式,但 NSCondition 信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而 dispatch_semaphore 能保存发送的信号。dispatch_semaphore 的核心是 dispatch_semaphore_t 类型的信号量。
  2. dispatch_semaphore_create(1) 方法可以创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 1。注意,这里的传入的参数必须大于或等于 0,否则 dispatch_semaphore_create 会返回 NULL。
  3. dispatch_semaphore_wait(semaphore, overTime); 方法会判断 semaphore 的信号值是否大于 0。大于 0 不会阻塞线程,消耗掉一个信号,执行后续任务。如果信号值为 0,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。
  4. dispatch_semaphore_signal(semaphore); 发送信号,如果没有等待的线程接受信号,则使 signal 信号值加一(做到对信号的保存)。
  5. 一个 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 互斥锁是一种超级易用的互斥锁,使用的时候:

  1. 只需要使用 pthread_mutex_init 初始化一个 pthread_mutex_t,
  2. pthread_mutex_lock 或者 pthread_mutex_trylock 来锁定 ,
  3. pthread_mutex_unlock 来解锁,
  4. 当使用完成后,记得调用 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解锁成功

由以上内容总结:

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

5. OSSpinLock

OSSpinLock 是一种自旋锁,和互斥锁类似,都是为了保证线程安全的锁。但二者的区别是不一样的,对于互斥锁,当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。但自选锁不一样,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。

只有加锁,解锁,尝试加锁三个方法。

常用相关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 基本一直,就不一一列出了。

总结

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 谈谈iOS中的几种锁

    1 前言 近日工作不是太忙,刚好有时间了解一些其他东西,本来打算今天上午去体检,但是看看天气还是明天再去吧,也有很大一个原因:就是周六没有预约上!闲话少说,这里简单对锁来个简单介绍分享. 2 目录 第一部分:什么是锁 第二部分:锁的分类 第三部分:锁的作用 第四部分:iOS中锁的实现 第一部分:什么是锁 从小就知道锁,就是家里门上的那个锁,用来防止盗窃的锁.它还有钥匙,用于开锁.不过这里的锁,并不是小时候认知的锁,而是站在程序员的角度的锁.这里我就按照我的理解来介绍一下锁. 在计算机科学中,锁是

  • 详解iOS 多线程 锁 互斥 同步

    在iOS中有几种方法来解决多线程访问同一个内存地址的互斥同步问题: 方法一,@synchronized(id anObject),(最简单的方法) 会自动对参数对象加锁,保证临界区内的代码线程安全 @synchronized(self) { // 这段代码对其他 @synchronized(self) 都是互斥的 // self 指向同一个对象 } 方法二,NSLock NSLock对象实现了NSLocking protocol,包含几个方法: lock,加锁 unlock,解锁 tryLock

  • IOS 线程死锁详细介绍

    iOS线程死锁 前言: 在chat view的开发过程中,添加了"混合标签添加与显示",app出现发送图片会出现卡死的情况,但过了大约30-40 second后会恢复正常. 问题分析: 因为没有任何报错与提示,只能根据表面现象慢慢分析,经过多次测试与观察得出以下规律: (1)发送表情与文本不会发生该情况,只有发送图片才会发生app界面卡死的情况.(主线程阻塞,与大文件上传有关)      (2)app卡死一定时间后会恢复正常,但时间不定,大约范围在30-40 second.(主线程解除

  • iOS滑动解锁、滑动获取验证码效果的实现代码

    最近短信服务商要求公司的app在获取短信验证码时加上校验码,目前比较流行的是采用类似滑动解锁的方式,我们公司采取的就是这种方式,设计图如下所示: 这里校验内部的处理逻辑不作介绍,主要分享一下界面效果的实现, 下面贴出代码: 先子类化UISlider #import <UIKit/UIKit.h> #define SliderWidth 240 #define SliderHeight 40 #define SliderLabelTextColor [UIColor colorWithRed:1

  • iOS实现手势解锁操作

    本文主要介绍通过手势识别实现手势解锁功能,这个方法被广泛用于手机解锁,密码验证,快捷支付等功能实现.事例效果如下所示. 首先,我们先分析功能的实现过程,首先我们需要先看大致的实现过程: 1.加载九宫格页面 2.实现按钮被点击及滑动过程中按钮状态的改变 3.实现滑动过程中的连线 4.绘制完毕后判定密码是否正确, 5.密码判定后实现跳转. 下面我们就来用代码实现上述五个过程. 1.加载九宫格界面 1.1九宫格内控件的分布 3*3 ,我们可以自定义view(包含3*3个按钮),添加到viewContr

  • 浅谈iOS中的锁的介绍及使用

    在平时的开发中经常使用到多线程,在使用多线程的过程中,难免会遇到资源竞争的问题,那我们怎么来避免出现这种问题那? 线程安全是什么? 当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕.简单来讲就是在同一时刻,对同一个数据操作的线程只有一个.只有确保了这样,才能使数据不会被其他线程影响.而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果. 比如写文件和读文件,当一个线程在写文件的时候,理论上来说,如果这个时候另一个线程来直接读取的话,那么得到的结

  • 浅谈多线程中的锁的几种用法总结(必看)

    一.ReentrantLock package com.ietree.basicskill.mutilthread.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Created by Administrator on 2017/5/17. */ public class UseReentrantLock { private Lock lock

  • 浅谈iOS中几个常用协议 NSCopying/NSMutableCopying

    1.几点说明 说到NSCopying和NSMutableCopying协议,不得不说的就是copy和mutableCopy. 如果类想要支持copy操作,则必须实现NSCopying协议,也就是说实现copyWithZone方法; 如果类想要支持mutableCopy操作,则必须实现NSMutableCopying协议,也就是说实现mutableCopyWithZone方法; iOS系统中的一些类已经实现了NSCopying或者NSMutableCopying协议的方法,如果向未实现相应方法的系

  • 浅谈iOS中三种生成随机数方法

    ios 有如下三种随机数方法: //第一种 srand((unsigned)time(0)); //不加这句每次产生的随机数不变 int i = rand() % 5; //第二种 srandom(time(0)); int i = random() % 5; //第三种 int i = arc4random() % 5 ; 注: ① rand()和random()实际并不是一个真正的伪随机数发生器,在使用之前需要先初始化随机种子,否则每次生成的随机数一样. ② arc4random() 是一个

  • 浅谈IOS中AFNetworking网络请求的get和post步骤

    1.首先通过第三方:CocoaPods下载AFNetworking 1.1.先找到要查找的三方库:pod search + AFNetworking 1.2.出来一堆列表页面,选择三方库最新版本命令,例如: pod 'MBProgressHUD','~>0.8'  (:q 返回) 1.3.创建工程,进入工程: cd + 工程路径 1.4.编辑工程的Podfile文件: vim Podfile 1.5.(platform :iOS, '8.0'
target "工程名" do
po

  • 浅谈iOS开发中static变量的三大作用

    (1)先来介绍它的第一条也是最重要的一条:隐藏 当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性.为理解这句话,我举例来说明.我们要同时编译两个源文件,一个是a.c,另一个是main.c. 下面是a.c的内容 char a = 'A'; // global variable void msg() { printf("Hello\n"); } 下面是main.c的内容 int main(void) { extern char a; // extern v

  • 浅谈iOS应用中的相关正则及验证

    1.手机号码的验证正则 正则表达式: ^((13[0-9])|(15[^4,\\D])|(18[0,0-9]))\\d{8}$ 详细解释 解释: •^...$: ^:开始 $:结束 中间为要处理的字串 •(13[0-9]): 以13开头接下来一位为0-9之间的数 13 : 以13开头 [0-9]:分割语法,13后面是0-9之间的数 •| : 或(or), 将前后两个匹配条件进行or运算 • (15[^4\\D]) : 以15开头接下来一位是除4之外的0-9数字 15 : 以15开头 [^4\\D

  • 浅谈Unity中IOS Build Settings选项的作用

    Run in Xcode as:分Release选项和Debug选项,分别对应的是Xcode中Scheme编辑的BuildConfiguration的Debug和Release选项 Symlink Unity libraries:这是专为IOS平台用的,是一个全名叫做Symbolic Link Unity Libraries的runtime库,勾选后,xcode工程会直接在Unity的安装路径下引用Unity ios runtime library,不勾选,这些ios动态库会直接copy到xco

  • 浅谈Python中的全局锁(GIL)问题

    CPU-bound(计算密集型) 和I/O bound(I/O密集型) 计算密集型任务(CPU-bound) 的特点是要进行大量的计算,占据着主要的任务,消耗CPU资源,一直处于满负荷状态.比如复杂的加减乘除.计算圆周率.对视频进行高清解码等等,全靠CPU的运算能力.这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数. 计算密集型任务由于主要消耗CPU资源,因

  • 浅谈Python3中datetime不同时区转换介绍与踩坑

    最近的项目需要根据用户所属时区制定一些特定策略,学习.应用了若干python3的时区转换相关知识,这里整理一部分记录下来. 下面涉及的几个概念及知识点: GMT时间:Greenwich Mean Time, 格林尼治平均时间 UTC时间:Universal Time Coordinated 世界协调时,可以认为是更精准的GMT时间,但两者误差极小,在1s以内,一般可视为等同 LMT:Local Mean Time, 当地标准时间 Python中的北京时间:Python的标准timezone中信息

随机推荐