iOS 常见锁的简单认识

1,182 阅读3分钟

iOS中常见的锁有哪些?

@synchronized,信号量dispatch_semaphore_tos_unfair_lock,实现了NSLocking的几个类(NSLockNSRecursiveLockNSConditionNSConditionLock

上面提到的都是互斥锁(即线程在尝试访问共享资源的时候,发现有锁,就休眠进行等待唤醒)。

关于锁的性能

网上有很多讨论锁性能的文章,但是我觉得这个大可不必。

首先肯定是功能简单,越贴近底层的锁性能最高。

比如实现了递归功能的锁性能一定是比不实现递归的锁要性能高。

但是抛开功能来谈性能,这不是有点无聊吗?

其次,网上的demo里都是在一个for循环里面,先加锁再解锁,没有考虑到多线程的情况。

我也不知道这样得出来的性能对比是否有意义。

拿了网上的demo,跑了一下。

循环在1万次的时候,可以看到各个锁的性能差别不大。最好的与最坏的差了三百多毫秒

因此我觉得在日常开发过程中,其实可以不考虑锁的性能,把精力放到功能实现上即可。

截屏2021-10-18 下午2.49.25.png

锁的一些简单介绍

@synchronized

使用代码一般如下:

@synchronized** ( self ) {
   // do something
}

功能非常强大。可以在多线程中保证数据安全,同时也支持递归使用。

内部的实现流程大致是(不考虑缓存的情况):

  • 加锁之前,先从线程的TLS存储空间中尝试获取数据data

  • 如果数据data存在,并且锁标识和当前传入的锁标识一样,那么就说明当前线程已经有锁,这个时候将lockcount计数加1,然后结束流程,进行mutex加锁操作。

  • 走到这里,说明没有data或者锁标识不一致。

  • 没有data的情况,直接创建data,并将threadcount计数加1,结束流程,进行mutex加锁操作。

  • 如果有data,只是锁的标识不一致,那就创建新的data,将其插入到链表的头部,结束流程,进行mutex加锁操作。

NSLock

是一个相对来说比较简单的互斥锁。

使用的时候需要注意:

  • 不支持递归,即同一条线程不能在未解锁之前进行加锁操作。

  • 加锁和解锁的操作必须在同一条线程进行

NSRecursiveLock/ pthread_mutex(recursive)

可以实现递归加锁功能。

NSCondition / NSConditionLock

在锁的基础上可以追加一些条件。

NSCondition的使用如下: 截屏2021-10-18 下午3.47.43.png

NSConditionLock使用如下: 截屏2021-10-18 下午4.13.46.png 可以看到,当condition不满足的时候,当前执行线程会被卡住,直到条件满足后继续执行。

dispatch_semaphore

利用 dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);dispatch_semaphore_signal(dispatch_semaphore_t dsema); 来实现锁的效果。

dispatch_semaphore_wait会将信号量减去1,如果此时信号量值小于0,就会卡住线程,等待唤醒 dispatch_semaphore_signal会将信号量加1,如果之前的信号量小于0,就会唤醒之前等待的线程。

os_unfair_lock

用来替代OSSpinLock,因为OSSpinLock存在优先级反转的问题,已经被弃用了。

OSSpinLock弃用原因:

高优先级线程始终在低优先级线程前执行。如果一个低优先级的线程先获得了锁并访问共享资源, 这个时候高优先级线程也尝试获得锁,就会出现高优先级线程在等待锁, 而低优先级线程在等高优先级线程用完CPU

值得注意的是os_unfair_lock是互斥锁,OSSpinLock是自旋锁。

读写锁

很早之前记得有个面试题,面试官让我实现:

  • 同一时间,只能有一个线程进行setter操作;

  • 同一时间,允许多个线程进行getter操作;

  • 同一时间,setter和getter操作互斥;

前不久看一个视频才明白这是一个读写锁的实际场景。

解决方案是利用栅栏函数+同步函数

image.png