iOS底层 - 锁的原理探索

603 阅读14分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. 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

分析完了执行流程,不禁好奇的问:

  1. 这个 NSConditionLock 和 NSCondition 有什么关系吗 ?
  2. initWithCondition:2 , 这里的 2 是一个什么东西呢 ?
  3. lockWhenCondition 是如何控制的呢 ?
  4. unlockWithCondition 又是做了什么才做呢 ?

下面,我们带着 这四个问题,通过打 断掉 调试, 来尝试探索下其内部流程,并回答下上面的四个问题:

image.png

在代码执行到 初始化 NSConditionLock 的时候,我们下下 符号断点 : initWithCondition,然后向下执行, 发现符号断点并没有卡住。 此处 断点没有卡住是正常的,因为这里的符号有问题,所以我们做一下修改:

image.png

然后重新运行:卡住断点,

image.png

项目是跑在我的真机上,我们 可以 通过 register read 读取下寄存器的内容:

最关健的细节是 :

  • x0 (消息的接受者)
  • x1 (当前的SEL)
  • x2 (方法参数)

-[NSConditionLock initWithCondition:]: 流程分析

image.png

打印出来之后和我们的这个方法全部都对上了。

在这里,因为都是汇编代码,在处理的过程中,我们需要筛选提炼抓住重点的执行语句来看,什么是重点需要看的呢? bl 做跳转相关处理的语句。

所以,讲汇编代码中的 bl 语句全部打上断点,我们再继续跟进流程。

image.png

接着向下执行:

image.png

再接着执行(大致是内存开辟的操作):

image.png

继续向下执行:

image.png

继续:

image.png

再接着,我们就来到了 ret 这里 的 return 返回;

大致看了下上面的流程,无非是一些初始化,开辟内存空间。 我们看到返回的对象是 NSConditionLock 的一个对象( condition = 2) 他有一个成员变量 NSCondition2

image.png

下面,我们继续看一下 lockWhenCondition:

-[NSConditionLock lockWhenCondition:]: 流程分析

image.png

继续向下执行,我们在第一个 bl 处: 是一个 NSDate 调用了 distantFuture 方法。

image.png

我们先不谈就为啥会调用到 Date ,我们先接着走完当前的流程 看是 一个什么样的过程。 继续:

image.png

NSConditionLock 调用了 lockWhenCondition:beforeDate: 方法, 第二个参数是 : _NSConstantDateDistantFuture

此处要调用 lockWhenCondition:beforeDate: 方法,那么,我们打上一个符号断点:

进入 lockWhenCondition:beforeDate: 之后,继续执行,看到 NSCondition 对象 调用了 lock 方法: image.png

最终 返回了一个 1 (不需要再等待):

image.png

最后 也是有一个 unlock 的操作。

-[NSConditionLock unlockWithCondition:]: 流程分析

符号断点已经加过,直接 向下执行:

image.png broadcast 广播了一下

最后,返回的时候:NSCondition 对象 做了 unlock 操作

image.png

源码对照

最后,我们通过源码,对照刚才的流程探索分析,看是否相同:

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 加锁 广播 解锁, 和最后探索的 流程一致。

总结

关于锁的使用,这一篇,我们就告一段落了。其实在实际使用中,什么锁都是可以用的,就是看根据具体的业务,使用哪种锁会更合适,效率会更高。经过这一篇的探索,我们关于开发中锁的使用,也更加清晰一点。希望对大家的理解能有锁帮助。大家,加油!!!