这是我参与8月更文挑战的第25天,活动详情查看:8月更文挑战
1. 锁的类型
自旋锁
:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待
。一旦获取了自旋锁,线程会一直保持该锁
,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销
,因此对于线程只会阻塞很短时间
的场合是有效的。
互斥锁
:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比 如全局变量)进行读写的机制有互斥和同步两个特点。当一个线程在进行任务的时候,别的线程就不能进行这个任务,这就是互斥。别的线程按照一定的顺序在这个线程完成后执行,这就是同步。该目的通过将代码切片成一个一个的临界区
而达成,互斥锁分为递归锁和非递归锁。这里属于互斥锁的有:
- NSLock
- pthread_mutex
- @synchronized
条件锁
:就是条件变量,当进程的某些资源要求不满足
时就进入休眠
,也就是锁住了。当资源被分配到
了,条件锁打开
,进程继续运行
- NSCondition
- NSConditionLock
递归锁
:就是同一个线程可以加锁N次
而不会引发死锁
- NSRecursiveLock
- pthread_mutex(recursive)
信号量(semaphore):
是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取值0/1时的特例
。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
- dispatch_semaphore
其实基本的锁就包括了三类 自旋锁
互斥锁
读写锁
,
其他的比如条件锁,递归锁,信号量都是上层的封装和实现
!
2. 锁的应用
这里的代码运行后会奔溃,崩溃的主要原因是testArray在某一瞬间变成了nil。
下面这段代码如果执行的话,就会有10个testMethod(10)在异步执行,那么如何确保他们一个接一个执行呢?也就是应该怎么加锁呢?
这里把锁加在testMethod(10)前后就解决了问题,这里也可以在testMethod声明之后加锁。这里不能在testMethod的代码块里面加锁,否则就会递归加锁,而NSLock不支持递归加锁。
那么如果加了递归锁,是否可以这样加锁呢?答案也是不行的,因为这里面是多线程环境,递归锁无法进行多线程可递归。而要多线程,可递归,则需要用@synchronized锁。
这里看到加了@synchronized锁之后是正常运行的。
3. 锁的底层实现
看到NSLock,这里面是遵守了NSLocking的协议,在foundation框架里面。
接下来去Foundation里面寻找NSLock,看到其底层实现是关于pthread_mutex的封装,其加锁解锁也是对
pthread_mutex相关API的调用。
在看到递归锁NSRecursiveLock,发现这里初始化的时候多了一个pthread_mutexattr_settype的调用,并且将类型设置为Recursive类型。
NSCondition里面则是添加了condition的处理。
4. NSConditionLock的探索
这里结合NSConditionLock来探索。
下面的执行结果是什么呢?这里执行的结果是3乱序,但是2会在1的前面。这里用了NSConditionLock,当condition等于2的时候,才会执行,所以2一定会比1先执行。线程3里面因为没有加条件,所以只是普通的加锁解锁,直接执行。
那么这里就出现了几个问题。
- NSConditionLock 和 NSCondition 有什么区别呢
- 初始化传的参数2是什么呢
- lockWhenCondition是如何控制的呢
- unlockWithCondition做了什么呢
4.1 [NSConditionLock initWithCondition :]
打下断点后运行。运行后添加符号断点NSConditionLock initWithCondition:
接下来就进入到initWithCondition方法的汇编了。
接着读取一下寄存器,发现都匹配上了。
接下来在汇编里面把bl全都打上断点,因为bl是跳转的地方。打完断点后继续运行来到第一个BL。
这里读取寄存器,发现是一个不知道什么的对象调用一个init方法,参数为2.
继续往下走,发现这里是一个NSConditionLock对象调用Init方法.参数为2。
继续往下走,发现这里是一个NSConditionLock对象调用zone方法.参数为2。
继续往下走,发现这里是一个NSCondition调用allocWithZone方法,参数没有了
继续往下走,发现这里是一个NSCondition对象调用init方法.
继续往下走,看到return,也就是[NSConditionLock initWithCondition :]这个方法的返回值。发现返回了NSConditionLock。
x/8gx 查看NSConditionLock对象的成员变量,发现NSCondition对象成了NSConditionLock对象的一个成员变量。这里的NSCondition对象是之前创建的那一个。之前的参数2也是NSConditionLock对象的一个成员变量。
到这里可以的得到,NSConditionLock里面封装了一个NSCondition,并且将之前传进来的2保存了下来。
- ?对象 调用init。
- [NSConditionLock init:2]
- [NSConditionLock zone:2]
- [NSCondition allocWithZone]
- [NSCondition init]
4.2 [NSConditionLock lockWhenCondition:]
接下来看lockWhenCondition。下一个[NSConditionLock lockWhenCondition:]的断点,运行来到汇编后,在bl打下断点。
读取一下寄存器,发现是一个NSDate对象调用了distantFuture方法。
继续往下走之后读取寄存器,发现是一个NSConditionLock对象调用了lockWhenCondition:beforeDate:方法。这里多读取一个x3因为是两个参数,x3感觉像是distantFuture方法的返回值。
继续下[NSConditionLock lockWhenCondition:beforeDate:]的符号断点,进入后向跳转的地方打下断点。这里8的断点没有东西,断到之后往下跳到13。
读取寄存器后发现,这里的NSCondition对象调用了一个lock。
往下跳发现没什么东西,继续往下走。
这里读取寄存器发现NSCondition对象调用了一个waitUntilDate的方法。参数为[NSConditionLock lockWhenCondition:beforeDate:]传进来的参数。
接着会跳到另外一个线程的[NSConditionLock lockWhenCondition:beforeDate:],因为这里就锁住了不走了。跳过来之后走到刚才那一步的下一步,发现没有什么东西。
继续往下走,发现调用了一个NSCondition对象的unlock方法。
继续往下走到return,发现返回了1。
[NSConditionLock lockWhenCondition:]
- [NSDate distantFuture:];
- [NSConditionLock lockWhenCondition:beforeDate:]
2.1 [NSCondition lock];
2.2 [ NSCondition waitUntilDate:]
2.3 [NSCondition unlock];
2.4 return 1
4.3 [NSConditionLock unlockWithCondition :]
接着往下走,就会来到unlockWithCondition方法里面,进来后下好断点,接着往下走。
读取寄存器,发现这里NSCondition对象调用了lock方法。
继续往下走读取寄存器,发现这里NSCondition对象调用了broadcast方法。
继续往下走,发现调用了一个NSCondition对象的unlock方法。
[NSConditionLock unlockWithCondition :]
- [NSCondition lock];
- [NSCondition broadcast];
- [NSCondition unlock];
这里走完unlockWithCondition之后,又回到了之前的线程,进行了unlock的操作。
然后return的地方返回值也为1.
然后接下来就走到了下面这里。
也就是说这里的执行顺序为:
- [conditionLock lockWhenCondition:1];
- [conditionLock lockWhenCondition:2];
- [conditionLock unlockWithCondition:1];
- [conditionLock unlockWithCondition:0];
5. 底层源码
这里去看一下底层源码是否是这样实现的。
首先看到init,这里确实有两个属性,一个_cond = NSCondition() 和一个_value: Int。并且在初始化的时候对_value进行赋值,赋值为传进来的参数condition。并且_cond进行了初始化。
接下来看[NSConditionLock lockWhenCondition:]。发现这里确实调用了Date.distantFuture 和 [NSConditionLock lockWhenCondition:beforeDate:]
接下来来到[NSConditionLock lockWhenCondition:beforeDate:],发现有_cond的lock和Unlock方法,也就是[NSCondition lock]; 和 [NSCondition unlock];并且当_thread != nil || _value != condition 的时候,就会调用!_cond.wait(until: limit)。超时的话就解锁,返回false。否则就正常解锁,返回true。
再来看[NSConditionLock unlockWithCondition :],看到这里做了_cond的lock和Unlock方法,并且中间调用了broadcast。
6. 读写锁
除了互斥锁和自旋锁,还有一个锁就是读写锁
。
读写锁实际是一种特殊的互斥锁
,它把对共享资源的访问者划分成读者
和写者
,读者
只对共享资源 进行读访问
,写者
则需要对共享资源进行写操作
。这种锁相对于自旋锁而言,能提高并发性
,因为在多处理器系统中,它允许同时有多个读者来访问共享资源
,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者
(与CPU数相关),但不能同时既有读者又有写者
。在读写锁保持期间也是抢占失效的。如果读写锁当前没有读者,也没有写者,那么写者
可以立刻获得读写锁
,否则它必须自旋
在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
实现读写锁需要做到:
- 多读单写
- 写和写互斥
- 读写互斥
- 写不能堵塞任务执行
用GCD实现读写锁的关键是栅栏函数
。
这里setter使用barrier_async
不会堵塞其他线程的执行,并且会等待前面的操作完成后在执行,并且能确保单写。
这里读的时候用的时候sync
,这样确保取值之后在返回,并且因为是多线程,所以可以实现多读
。而这里的sync又能确保读写互斥
。