iOS 锁

我的博客, 各位看官有时间赏光

  • 我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源, 应运而生。
  • 是最常用的同步工具:一段代码段在同一个时间只能允许被一个线程访问,比如一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。

开篇

讲iOS 锁 自然绕不开大神ibireme前段时间的一篇文章 《不再安全的 OSSpinLock》

分析各种锁的性能的图表

ibireme 文中论述了OSSpinLock为什么已经不再是线程安全,想具体了解的童鞋可以去大神的原文了解,我就在下面简要介绍一下:
(课外知识,借鉴!)深入理解 iOS 开发中的锁

OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。
高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。
这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

为什么忙等会导致低优先级线程拿不到时间片?这还得从操作系统的线程调度说起。

现代操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片(quantum),通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。

OSSpinLock 自旋锁 实现机制:忙等 操作重点:原子操作


需导入头文件:
#import

OS_SPINLOCK_INIT: 默认值为 0,在 locked 状态时就会大于 0,unlocked状态下为 0

OSSpinLockLock(&oslock):上锁,参数为 OSSpinLock 地址

OSSpinLockUnlock(&oslock):解锁,参数为 OSSpinLock 地址

OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
__block OSSpinLock oslock = OS_SPINLOCK_INIT;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"准备上锁");
    OSSpinLockLock(&oslock);
    OSSpinLockUnlock(&oslock);
    NSLog(@"解锁成功");
});

dispatch_semaphore 信号量

** 实现原理:不是使用忙等,而是阻塞线程并睡眠,主动让出时间片,需要进行上下文切换**


主动让出时间片并不总是代表效率高。
让出时间片会导致操作系统切换到另一个线程,这种上下文切换通常需要 10 微秒左右,而且至少需要两次切换。
如果等待时间很短,比如只有几个微秒,忙等就比线程睡眠更高效。(系统任务少,下次分配时间片的间隔也不长的情况下,忙等的线程可以在较短的时间后就可以再次分配到时间片  作者就是在比较这个等待的时间间隔和让出时间片 付出的两次切换上下文花费的时间做比较)
dispatch_semaphore_create(1): 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句

dispatch_semaphore_wait(signal, overTime):可以理解为 lock,会使得 signal 值 -1

dispatch_semaphore_signal(signal):可以理解为 unlock,会使得 signal值 +1
dispatch_semaphore_t signal = dispatch_semaphore_create(1); //传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   NSLog(@" 等待");
   dispatch_semaphore_wait(signal, overTime); //signal 值 -1
   dispatch_semaphore_signal(signal); //signal 值 +1
   NSLog(@"发送信号");
});

对于dispatch_semaphore网上有个很形象的例子:

停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值(signal)就相当于剩余车位的数目,
dispatch_semaphore_wait 函数就相当于来了一辆车,
dispatch_semaphore_signal 就相当于走了一辆车。
停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),
调用一次 dispatch_semaphore_signal,剩余的车位就增加一个;
调用一次dispatch_semaphore_wait 剩余车位就减少一个;
当剩余车位为 0 时,再来车(即调用 dispatch_semaphore_wait)就只能等待。
有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就想把车停在这,所以就一直等下去。

pthread 表示 POSIX thread 定义了一组跨平台的线程相关的 API

pthread_mutex 互斥锁 实现原理:不是使用忙等,而是阻塞线程并睡眠,主动让出时间片,需要进行上下文切换


ibireme 在《不再安全的 OSSpinLock》这篇文章中提到性能最好的 OSSpinLock 已经不再是线程安全的并把自己开源项目中的 OSSpinLock 都替换成了 pthread_mutex。

了解互斥锁POSIX

POSIX和dispatch_semaphore_t很像,但是完全不同。
POSIX是Unix/Linux平台上提供的一套条件互斥锁的API。

新建一个简单的POSIX互斥锁,引入头文件#import 声明
初始化一个pthread_mutex_t的结构。
使用pthread_mutex_lock和pthread_mutex_unlock函数。
调用pthread_mutex_destroy来释放该锁的数据结构。

条件变量

POSIX还可以创建条件锁,提供了和NSCondition一样的条件控制,初始化互斥锁同时使用pthread_cond_init来初始化条件数据结构,

   // 初始化
   int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);

   // 等待(会阻塞)
   int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);

   // 定时等待
   int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);

   // 唤醒
   int pthread_cond_signal (pthread_cond_t *cond);

   // 广播唤醒
   int pthread_cond_broadcast (pthread_cond_t *cond);

   // 销毁
   int pthread_cond_destroy (pthread_cond_t *cond);

POSIX还提供了很多函数,有一套完整的API,包含Pthreads线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。推荐一篇详细文章,但不是基于iOS的,是基于Linux的,但是介绍的非常详细 Linux 线程锁详解

使用需导入头文件:#import

互斥锁的常见用法如下:

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 创建锁

pthread_mutex_lock(&mutex); // 申请锁
   // 临界区
pthread_mutex_unlock(&mutex); // 释放锁
static pthread_mutex_t pLock;
pthread_mutex_init(&pLock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"准备上锁");
    pthread_mutex_lock(&pLock);
    sleep(3);
    pthread_mutex_unlock(&pLock);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"准备上锁");
    pthread_mutex_lock(&pLock);
    pthread_mutex_unlock(&pLock);
});
  • pthread_mutex 中也有个pthread_mutex_trylock(&pLock),和上面提到的 OSSpinLockTry(&oslock)区别在于,前者可以加锁时返回的是 0,否则返回一个错误提示码;后者返回的 YES和NO

**由于 pthread_mutex 有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。 **

pthread_mutex(recursive) 递归锁


经过上面几种例子,我们可以发现:
加锁后只能有一个线程访问该对象,后面的线程需要排队,
并且 lock 和 unlock 是对应出现的,
同一线程多次 lock 是不允许的,
而递归锁允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。

static pthread_mutex_t pLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁

pthread_mutex_init(&pLock, &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(&pLock);
      if (value > 0) {
          RecursiveBlock(value - 1);
      }
      pthread_mutex_unlock(&pLock);
  };
  RecursiveBlock(5);
});

上面的代码如果我们用

  • 互斥锁 pthread_mutex_init(&pLock, NULL) 初始化会出现死锁的情况,
  • 递归锁pthread_mutexattr_init(&attr);能很好的避免这种情况的死锁;

一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁或释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁。


pthread_rwlock 读写锁


在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量
但读是可以的,多个线程读取时没有问题的。

  • 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行。
  • 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。
// 初始化
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER
// 读模式
pthread_rwlock_wrlock(&lock);
// 写模式
pthread_rwlock_rdlock(&lock);
// 读模式或者写模式的解锁
pthread_rwlock_unlock(&lock);

NSLock 普通锁

** 实质:NSLock 只是在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。**


NSLock 是 Objective-C 以对象的形式暴露给开发者的一种锁,它的实现非常简单,通过宏,定义了 lock 方法:

#define    MLOCK \
- (void) lock\
{\
int err = pthread_mutex_lock(&_mutex);\
// 错误处理 ……
}
这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部 pthread_mutex 互斥锁的类型不同。通过宏定义,可以简化方法的定义。

NSLock 比 pthread_mutex 略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。
lock、unlock:不多做解释,和上面一样

trylock:能加锁返回 YES 并执行加锁操作,相当于 lock,反之返回 NO

lockBeforeDate:这个方法表示会在传入的时间内尝试加锁,若能加锁则执行加锁操作并返回 YES,反之返回 NO
NSLock *lock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"尝试加锁");
    [lock lock];
    [lock unlock];
    NSLog(@"解锁成功");
});

NSCondition 条件锁


NSCondition 其实是封装了一个互斥锁和条件变量, 它把前者的 lock 方法和后者的 wait/signal 统一在 NSCondition 对象中,暴露给使用者:

- (void) signal {
pthread_cond_signal(&_condition);
}

其实这个函数是通过宏来定义的,展开后就是这样
- (void) lock {
int err = pthread_mutex_lock(&_mutex);
}

它的加解锁过程与 NSLock 几乎一致,理论上来说耗时也应该一样(实际测试也是如此)。在图中显示它耗时略长,我猜测有可能是测试者在每次加解锁的前后还附带了变量的初始化和销毁操作

NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的
条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。

wait:进入等待状态

waitUntilDate::让一个线程等待一定的时间

signal:唤醒一个等待的线程

broadcast:唤醒所有等待的线程
NSCondition *cLock = [NSCondition new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [cLock lock];
    [cLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
    [cLock unlock];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);
    NSLog(@"唤醒一个等待的线程");
    [cLock signal];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);
    NSLog(@"唤醒所有等待的线程");
    [cLock broadcast];
});

NSRecursiveLock 递归锁


上文已经说过,递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。

NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE, 后者的类型为 PTHREAD_MUTEX_ERRORCHECK。

上面已经大概介绍过了:
递归锁可以被同一线程多次请求lock,而不会引起死锁。这主要是用在循环或递归操作中

不使用递归锁的造成死锁:

NSLock *rLock = [NSLock new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  static void (^RecursiveBlock)(int);
  RecursiveBlock = ^(int value) {
      [rLock lock];
      if (value > 0) {
          NSLog(@"线程%d", value);
          RecursiveBlock(value - 1);
      }
      [rLock unlock];
  };
  RecursiveBlock(4);
});

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

使用需导入头文件:#import

互斥锁的常见用法如下:

pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr) // 创建锁

pthread_mutex_lock(&mutex); // 申请锁
   // 临界区
pthread_mutex_unlock(&mutex); // 释放锁

NSConditionLock 条件锁


NSConditionLock 借助 NSCondition 来实现,
它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。
NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:

- (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;
* 相比于 NSLock 多了个 condition 参数,我们可以理解为一个条件标示。

NSConditionLock *cLock = [[NSConditionLock alloc] initWithCondition:0];

//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  if([cLock tryLockWhenCondition:0]){
      NSLog(@"线程1");
     [cLock unlockWithCondition:1];
  }else{
       NSLog(@"失败");
  }
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [cLock lockWhenCondition:3];
  NSLog(@"线程2");
  [cLock unlockWithCondition:2];
});

//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [cLock lockWhenCondition:1];
  NSLog(@"线程3");
  [cLock unlockWithCondition:3];
});

* 我们在初始化 NSConditionLock 对象时,给了他的标示为 0
* 执行 tryLockWhenCondition:时,我们传入的条件标示也是 0,所 以线程1 加锁成功
* 执行 unlockWithCondition:时,这时候会把condition由 0 修改为 1
* 因为condition 修改为了  1, 会先走到 线程3,然后 线程3 又将 condition 修改为 3
* 最后 走了 线程2 的流程
从上面的结果我们可以发现,NSConditionLock 还可以实现任务之间的依赖。

@synchronized 互斥锁


这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。
我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。
这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对象去哈希值来得到对应的互斥锁。
@synchronized 相信大家应该都熟悉,它的用法应该算这些锁中最简单的:
@synchronized 结构所做的事情跟锁(lock)类似:它防止不同的线程同时执行同一段代码。但在某些情况下,相比于使用 NSLock 创建锁对象、加锁和解锁来说,
@synchronized 用着更方便,可读性更高。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  @synchronized (self) {
      sleep(2);
  }
});

延伸:


锁是一种同步机制;为了防止同一资源被多个线程同时访问引发数据错乱和数据安全等问题

GCD线程阻断dispatch_barrier_async/dispatch_barrier_sync


dispatch_barrier_async/dispatch_barrier_sync在一定的基础上也可以做线程同步,会在线程队列中打断其他线程执行当前任务,
也就是说只有用在并发的线程队列中才会有效,因为串行队列本来就是一个一个的执行的,你打断执行一个和插入一个是一样的效果。
两个的区别是是否等待任务执行完成。

  • 注意:如果在当前线程调用dispatch_barrier_sync打断会发生死锁。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  @synchronized (self) {
      sleep(2);
  }
});

NSDistributedLock 分布式锁 (MAC开发中的跨进程的分布式锁)


NSDistributedLock是MAC开发中的跨进程的分布式锁,底层是用文件系统实现的互斥锁。
NSDistributedLock没有实现NSLocking协议,所以没有lock方法,取而代之的是非阻塞的tryLock方法。

NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
  while (![lock tryLock])
  {
      sleep(1);
  }

  //do something
  [lock unlock];
当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.其他应用也不能访问受保护的共享资源。
在这种情况下,你可以使用breadLock方法来打破现存的锁以便你可以获取它。
但是通常应该避免打破锁,除非你确定拥有进程已经死亡并不可能再释放该锁。

了解死锁


概念:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

产生死锁的4个必要条件

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。


结束语

iOS的锁有这么多,需要根据自己的需要选择合适的锁.但是线程安全是选择是的首要标准!
网上有很多人都对iOS 的锁进行了性能测试(只能作为参考),感兴趣的可以去看看,我也推荐两篇:
iOS同步对象性能对比(iOS锁性能对比)iOS多线程-各种线程锁的简单介绍

参考资料
pthread_mutex_lock
ThreadSafety
Difference between binary semaphore and mutex
关于 @synchronized,这儿比你想知道的还要多
pthread_mutex_lock.c 源码
[Pthread] Linux中的线程同步机制(二)--In Glibc
pthread的各种同步机制
pthread_cond_wait
Conditional Variable vs Semaphore
NSRecursiveLock Class Reference
Objective-C中不同方式实现锁(二)
相关链接
https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000344.html
http://mjtsai.com/blog/2015/12/16/osspinlock-is-unsafe/
http://engineering.postmates.com/Spinlocks-Considered-Harmful-On-iOS/
https://twitter.com/steipete/status/676851647042203648

求知若饥 虚心若愚

你可能感兴趣的:(iOS 锁)