这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
- iOS底层 - @synchronized 流程分析
以上内容的总结专栏
细枝末节整理
前言
上一篇,我们分析了@synchronized
这把互斥锁的基本使用和其内部流程,接下来我们逐一将iOS开发中常用的锁做一个探索,今天我们就从 NSLock
这把互斥锁的流程吧。
关于锁的使用,我们在 iOS底层原理探索 - 锁的基本使用 中有介绍使用方式。
NSLock - NSRecursiveLock
从一个经典的案例出发,对比下 NSLock 和 NSRecursiveLock;
for (int i= 0; i<1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d-%@", value, [NSThread currentThread]);
testMethod(value - 1);
}
};
testMethod(10);
});
}
运行结果:
current value = 10-<NSThread: 0x60000154d200>{number = 6, name = (null)}
current value = 10-<NSThread: 0x60000150d840>{number = 5, name = (null)}
current value = 10-<NSThread: 0x60000154c8c0>{number = 9, name = (null)}
current value = 10-<NSThread: 0x600001527800>{number = 12, name = (null)}
current value = 10-<NSThread: 0x600001560b80>{number = 11, name = (null)}
current value = 10-<NSThread: 0x600001551280>{number = 7, name = (null)}
current value = 10-<NSThread: 0x600001519480>{number = 4, name = (null)}
current value = 10-<NSThread: 0x60000155c000>{number = 3, name = (null)}
current value = 10-<NSThread: 0x600001527e40>{number = 8, name = (null)}
current value = 9-<NSThread: 0x60000150d840>{number = 5, name = (null)}
current value = 9-<NSThread: 0x60000154c8c0>{number = 9, name = (null)}
current value = 10-<NSThread: 0x600001548200>{number = 14, name = (null)}
current value = 9-<NSThread: 0x60000154d200>{number = 6, name = (null)}
current value = 9-<NSThread: 0x600001527800>{number = 12, name = (null)}
current value = 9-<NSThread: 0x600001560b80>{number = 11, name = (null)}
...
current value = 2-<NSThread: 0x600001564080>{number = 33, name = (null)}
current value = 3-<NSThread: 0x60000150ef40>{number = 28, name = (null)}
current value = 1-<NSThread: 0x600001564080>{number = 33, name = (null)}
current value = 2-<NSThread: 0x60000150ef40>{number = 28, name = (null)}
current <NSThread: 0x600001564080>{number = 33, name = (null)}
current value = 1-<NSThread: 0x60000150ef40>{number = 28, name = (null)}
current <NSThread: 0x60000150ef40>{number = 28, name = (null)}
current value = 2-<NSThread: 0x60000150f380>{number = 40, name = (null)}
current <NSThread: 0x6000015740c0>{number = 31, name = (null)}
current value = 1-<NSThread: 0x60000150f380>{number = 40, name = (null)}
current <NSThread: 0x60000150f380>{number = 40, name = (null)}
直接运行,可以看到是乱序的,因为多线程的原因,再加上递归操作会出现这样的问题。
为了顺序打印,我们要思考加锁的问题,考虑把锁加在那里 从而 实现我们的目的。
简单分析一下,可以找到 我们这段 业务的 核心代码 是 testMethod(10);
这一个方法的调用, 所以我们在 这一行的上下 分别 使用 NSLock 或 NSRecursiveLock 分别加锁 和 解锁 即可。
这里需要注意,一般我们会把锁加在 block 执行的代码块中,这样会导致在 block 执行的开头 一只进行 加锁,却等不到解锁,所以就出现了问题。
NSLock 是非递归的锁,所以这里递归加锁就出现 了问题。 这个时候我们尝试下 NSRecursiveLock 这把锁 可以看到它是可以完整的打印一遍 10 - 0, 并不是 1000 遍, 也就是 虽然 NSRecursiveLock 这把锁支持递归性,但是 对于多线程的 递归使用 也是会有问题的。
current value = 10-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 9-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 8-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 7-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 6-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 5-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 4-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 3-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 2-<NSThread: 0x600000211440>{number = 6, name = (null)}
current value = 1-<NSThread: 0x600000211440>{number = 6, name = (null)}
这时就想到了上一篇 iOS底层 - @synchronized 流程分析 @synchronized 这把锁,可以多线程递归操作。 所以,我们在 block 中的业务使用 @synchronized 锁,这样也就可以完美的实现顺序打印。
底层实现
我们在OC环境中,使用 NSLock 中,通过点进去 可以看到 :
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@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
其实,这时一个来自于 NSObject 关于 NSLocking 的锁的协议,所以只要我们实现这个协议就会有这两个方法(很多的锁都会有这两个方法)。 但是我们看不到这两个方法的实现。下面,我们就结合swift的源码,来看下这两个方法的具体实现。
NSLock
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
open func lock() {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
NSRecursiveLock
internal var mutex = _RecursiveMutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
super.init()
#if os(Windows)
InitializeCriticalSection(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
#if CYGWIN
var attrib : pthread_mutexattr_t? = nil
#else
var attrib = pthread_mutexattr_t()
#endif
withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
open func lock() {
#if os(Windows)
EnterCriticalSection(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
LeaveCriticalSection(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
总结
可见, NSLock 和 NSRecursiveLock 底层是对于 pthread_mutex_lock
这把锁的封装。然而,为什么 NSRecursiveLock 具备递归性呢? 因为在初始化的时候, pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
有一个 settype 的操作, 标识。
NSCondition
NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主为了当检测条件时保护数据源,执行条件引发的任务; 线程检查器 主要是根据条件决定是否继续运行线程,及线程是否被阻塞。
- [condition lock] 一般用于多线程同时访问、修改同一数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在
lock
外等待,直到unlock
,才可以访问。 - [condition unlock] 与 lock 同时使用。
- [condition wait] 让当前线程处于等待状态。
- [condition signal] CPU 发出信号告诉线程不用在等待,可以继续执行。
生产消费模型
下面我们通过一个生产者消费者的模型来讲解下 这把 NSCondition 的使用。
生产者负责制票, 消费者负责买票。我们用 ticketCount 来表示当前的票量。 生产就是 对 ticketCount+1; 消费者就是对 tichekCount-1(当ticketCount为0时,等待有票);如下:
- 生产者
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
- 消费者
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
return;
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
同样的,在多线程的情况下,我们既有售票又有买票,就会产生数据票数上的混乱(我们挑一部分的日志来分析一下):
生产一个 现有 count 1
等待 count 0
消费一个 还剩 count 0
生产一个 现有 count 1
生产一个 现有 count 2
消费一个 还剩 count 1
消费一个 还剩 count 0
生产一个 现有 count 1
消费一个 还剩 count 1
生产一个 现有 count 2
生产一个 现有 count 2
消费一个 还剩 count 0
消费一个 还剩 count 1
生产之后,有一个票了却在等待因为日志显示有0张票,还有就是在下面已经消费后只剩0个,接着生产了一个,消费一个后日志显示还剩1个。这就是数据在多线程下的混乱。 下面,我们结合这样的场景,卡你啊NSCondition,如何应: 首先就是制票的时候,我们要加上锁:
[condition lock]; //加锁,防止多线程数据不安全
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[condition unlock];// 解锁
[condition signal];// 发出信号
买票,我们这样来加锁:
[condition lock]; //加锁,防止多线程数据不安全
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[condition wait]; //等待制票完成
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[condition unlock]; //解锁
如此,再次进行多线程的制票售票后日志如下:
生产一个 现有 count 1
消费一个 还剩 count 0
等待 count 0
生产一个 现有 count 1
生产一个 现有 count 2
消费一个 还剩 count 1
消费一个 还剩 count 0
生产一个 现有 count 1
生产一个 现有 count 2
生产一个 现有 count 3
生产一个 现有 count 4
生产一个 现有 count 5
生产一个 现有 count 6
生产一个 现有 count 7
生产一个 现有 count 8
消费一个 还剩 count 7
生产一个 现有 count 8
生产一个 现有 count 9
生产一个 现有 count 10
消费一个 还剩 count 9
生产一个 现有 count 10
消费一个 还剩 count 9
生产一个 现有 count 10
完美。
底层实现
和上面的NSLock和NSRecursiveLock一样,也是对于 pthread_mutex_lock
的封装, 只不过是 多了 cond
的相关处理。
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
public override init() {
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(cond)
#else
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
#endif
}
deinit {
#if os(Windows)
// SRWLock do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
#endif
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}
open func lock() {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
#else
pthread_mutex_unlock(mutex)
#endif
}
open func wait() {
#if os(Windows)
SleepConditionVariableSRW(cond, mutex, WinSDK.INFINITE, 0)
#else
pthread_cond_wait(cond, mutex)
#endif
}
open func wait(until limit: Date) -> Bool {
#if os(Windows)
return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
#else
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
#endif
}
open func signal() {
#if os(Windows)
WakeConditionVariable(cond)
#else
pthread_cond_signal(cond)
#endif
}
NSConditionLock
NSConditionLock 是锁, 一旦一个线程获得锁,其他线程一定等待。
- [conditionlock lock] 表示 conditionlock 期待获得锁,如果没有其他线程获得锁(不需要判断内部的condition)那它能执行此行一下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件锁),则等待,直至其他线程解锁。
- [conditionlock lockWhenCondition: A条件] 表示如果没有其他线程获得该锁,但该锁内部的condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于条件A,并且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的完成,直至它解锁。
- [conditionlock unlockWhenCondition: A条件] 表示释放锁, 同时把内部的condition设置为A条件。
- return [conditionlock lockWhenCondition: A条件 beforeData: A时间] 表示如果被锁定(没获得锁)并超过该时间则不再阻塞线程。但是注意:返回值是NO,它没有改变锁的状态,这个函数的目的在于可以实现两种状态下的处理。
例题
同样的,为了加深对与 NSConditionLock 这把锁的理解,我们通过一到例题来开始今天的分析:
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
此时此刻,不妨,你先猜一下 打印顺序是什么?
好,揭晓答案, 因为 Condition初始化的时候,传了一个2; 所以2 和 3 会先执行,但是因为2任务有一个延迟操作,所以,大概率是 3 先执行,然后 是2 , 1最后执行;运行项目,结果也是如此:
线程 3
线程 2
线程 1
分析完了执行流程,不禁好奇的问:
- 这个 NSConditionLock 和 NSCondition 有什么关系吗 ?
- initWithCondition:2 , 这里的 2 是一个什么东西呢 ?
- lockWhenCondition 是如何控制的呢 ?
- unlockWithCondition 又是做了什么才做呢 ?
下面,我们带着 这四个问题,通过打 断掉 调试, 来尝试探索下其内部流程,并回答下上面的四个问题:
在代码执行到 初始化 NSConditionLock 的时候,我们下下 符号断点 : initWithCondition
,然后向下执行, 发现符号断点并没有卡住。 此处 断点没有卡住是正常的,因为这里的符号有问题,所以我们做一下修改:
然后重新运行:卡住断点,
项目是跑在我的真机上,我们 可以 通过 register read
读取下寄存器的内容:
最关健的细节是 :
- x0 (消息的接受者)
- x1 (当前的SEL)
- x2 (方法参数)
-[NSConditionLock initWithCondition:]: 流程分析
打印出来之后和我们的这个方法全部都对上了。
在这里,因为都是汇编代码,在处理的过程中,我们需要筛选提炼抓住重点的执行语句来看,什么是重点需要看的呢? bl
做跳转相关处理的语句。
所以,讲汇编代码中的 bl
语句全部打上断点,我们再继续跟进流程。
接着向下执行:
再接着执行(大致是内存开辟的操作):
继续向下执行:
继续:
再接着,我们就来到了 ret 这里 的 return 返回;
大致看了下上面的流程,无非是一些初始化,开辟内存空间。 我们看到返回的对象是 NSConditionLock 的一个对象( condition = 2) 他有一个成员变量 NSCondition
和 2
。
下面,我们继续看一下 lockWhenCondition:
-[NSConditionLock lockWhenCondition:]: 流程分析
继续向下执行,我们在第一个 bl 处:
是一个 NSDate
调用了 distantFuture
方法。
我们先不谈就为啥会调用到 Date ,我们先接着走完当前的流程 看是 一个什么样的过程。 继续:
NSConditionLock
调用了 lockWhenCondition:beforeDate: 方法, 第二个参数是 : _NSConstantDateDistantFuture
此处要调用 lockWhenCondition:beforeDate: 方法,那么,我们打上一个符号断点:
进入 lockWhenCondition:beforeDate: 之后,继续执行,看到 NSCondition 对象 调用了 lock 方法:
最终 返回了一个 1 (不需要再等待):
最后 也是有一个 unlock 的操作。
-[NSConditionLock unlockWithCondition:]: 流程分析
符号断点已经加过,直接 向下执行:
broadcast 广播了一下
最后,返回的时候:NSCondition 对象 做了 unlock 操作
源码对照
最后,我们通过源码,对照刚才的流程探索分析,看是否相同:
NSConditionLock
内部有一个 NSCondition 成员变量,初始化的时候会对其进行赋值操作,这也就解释了,在 初始化 NSConditionLock 时,我们看到的 初始化 开辟内存的操作。
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_cond.broadcast()
_cond.unlock()
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
open func unlock(withCondition condition: Int) {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_value = condition
_cond.broadcast()
_cond.unlock()
}
}
在 lockWhenCondition 的时候, 有一个 Date.distantFuture
者也就解释 了在流程中,我们看到的 NSDate
调用了 distantFuture
在 锁住之后, 进行 比对相关的操作,在 if !_cond.wait(until: limit)
会调用等待,如果超时,则会 解锁 并 返回false。 最后 会对 thread 进行处理,最后返回一个 true 也就是 我们探索流程中的 返回了 1。
最后的unlock , condition 加锁 广播 解锁, 和最后探索的 流程一致。
总结
关于锁的使用,这一篇,我们就告一段落了。其实在实际使用中,什么锁都是可以用的,就是看根据具体的业务,使用哪种锁会更合适,效率会更高。经过这一篇的探索,我们关于开发中锁的使用,也更加清晰一点。希望对大家的理解能有锁帮助。大家,加油!!!