这两天翻看 ibireme 大神 《不再安全的 OSSpinLock》 这篇文章,看到文中分析各种锁之前的性能的图表:
我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁 应运而生。
用锁的场景:多条线程存在同时操作(删、查、读、写)同一个文件or对象or变量。如果不是同时或者不是同一个那就不用加锁了。
1)OSSpinLock
OSSpinLock是在iOS10前还算比较常见的一钟锁,其是"忙等"的锁,所以适用于轻量级的操作,比如基本数据类型的加减,如int 的-1,+1操作,“忙等”的锁,大致的解析就是会一直 while(目标锁还未释放),然后一直执行,所以会很耗cpu的性能。
OSSpinlock 在ios10以前使用,在ios10以后已经弃用
改用了os_unfair_lock 来实现了
os_unfair_lock
os_unfair_lock是在iOS10之后为了替代自旋锁OSSpinLock而诞生的,主要是通过线程休眠的方式来继续加锁,而不是一个“忙等”的锁。猜测是为了解决自旋锁的优先级反转的问题。
dispatch_semaphore
假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这种情况下,该如何处理呢?
我们要下载很多图片,并发异步进行,每个下载都会开辟一个新线程,可是我们又担心太多线程肯定cpu吃不消,那么我们这里也可以用信号量控制一下最大开辟线程数
信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程
dispatch_semaphore_create(1): 传入值必须 >=0, 若传入为 0 则阻塞线程并等待overTime,时间到后会执行其后的语句
dispatch_semaphore_wait(signal, overTime):可以理解为 lock,会使得 signal 值 -1
dispatch_semaphore_signal(signal):可以理解为 unlock,会使得 signal 值 +1
注意,正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。
信号量设为1表示同一时间只能有一个线程访问资源,单纯使用信号量并不是保证有序,还和任务数量有关,只是控制资源的同一时间访问个数;
2)pthread_mutex
ibireme 在《不再安全的 OSSpinLock》这篇文章中提到性能最好的 OSSpinLock 已经不再是线程安全的并把自己开源项目中的 OSSpinLock 都替换成了 pthread_mutex。
pthread_mutex 中也有个pthread_mutex_trylock(&pLock),和上面提到的 OSSpinLockTry(&oslock)区别在于,前者可以加锁时返回的是 0,否则返回一个错误提示码;后者返回的 YES和NO
pthread_mutex(recursive)
经过上面几种例子,我们可以发现:加锁后只能有一个线程访问该对象,后面的线程需要排队,并且 lock 和 unlock 是对应出现的,同一线程多次 lock 是不允许的,而递归锁允许同一个线程在未释放其拥有的锁时反复对该锁进行加锁操作。
上面的代码如果我们用 pthread_mutex_init(&pLock, NULL) 初始化会出现死锁的情况,递归锁能很好的避免这种情况的死锁;
Nslock
NSCondition
唤醒等待的线程
NSRecursiveLock
上面已经大概介绍过了:
递归锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。
@synchronized
这个@synchronized的代码块和前面例子中的[ _lock unlock]、[ _lock unlock]的作用相同作用效果。你可以把它理解成把self当作一个NSLock来对self进行加锁。在运行{后的代码前获取锁,并在运行}后的其他代码前释放这个锁。这非常的方便,因为这意味着你永远不会忘了调用unlock;
对于每个加了同步的对象,`Objective-C的运行时都会给其分配一个递归锁,并且保存在一个哈希表中。
1)注意不要往@synchronized代码块中传入nil!这会毁掉代码的线程安全性
2)我们从 clang转换之后的代码可知,@synchronized第一步就是将加锁条件进行强引用给 id类型的 _sync_objc变量,所以此处不接受非OC对象作为加锁条件。
3) @synchronized会对加锁条件进行强引用,原因在于若不进行引用,直接对加锁条件进行操作,那么如果在临界区中对加锁条件进行改变,那么在后续的 objc_sync_exit中获取到的 SyncData就会发生变化,最终导致加锁解锁操作不对称。
4)既然@synchronized对加锁条件进行了强引用保护,那么是否可以在临界区代码中对加锁条件进行更改?
不建议在临界区代码中对加锁条件进行更改的操作。
原因在于若在临界区代码中对加锁条件进行更改,那么此时如果再次对该加锁条件进行加锁,此时获取的 SyncData为不同对象对应的值,虽说也能成功加锁,但是无法保证与第一次加锁线程互斥,可能造成业务逻辑的错误。
5) 是否可以对所有需要锁的操作都使用同一个加锁条件?
不建议对所有需要锁的操作使用同一个加锁条件。
原因在于当某个操作对加锁条件进行加锁后,若其他与该操作无关的操作再对加锁条件进行加锁时,需等到前一个操作执行完毕,这可能造成无关操作多余无用的等待时间,造成程序效率低下。
所以建议对涉及共同资源的操作使用同一个加锁条件进行加锁,相互无关的操作使用不同的加锁条件加锁。
NSConditionLock 条件锁
相比于 NSLock 多了个 condition 参数,我们可以理解为一个条件标示。
我们在初始化 NSConditionLock 对象时,给了他的标示为 0
执行 tryLockWhenCondition:时,我们传入的条件标示也是 0,所 以线程1 加锁成功
执行 unlockWithCondition:时,这时候会把condition由 0 修改为 1
因为condition 修改为了 1, 会先走到 线程3,然后 线程3 又将 condition 修改为 3
最后 走了 线程2 的流程
从上面的结果我们可以发现,NSConditionLock 还可以实现任务之间的依赖
网友评论