OC底层原理探索之NS(Recursive)Lock和NSCondation(Lock)锁

2,635 阅读7分钟

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

@synchronized

上篇我们分析了@synchronized的结构,那么SyncData是怎么创建的呢?不同的对象或者不同的线程又是怎么处理的呢?我们知道SyncData的创建在以下的代码中

    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;

问题是什么情况下才会进入到这里。我们可以代码验证下,在上节介绍到,模拟器的情况下,这个StripeMap有64位,这里为了增加哈希冲突概率我们修改为1.

class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 1 };
#endif
}

代码验证的步骤,首先在libObjc源码工程下写入这行代码,并在SyncData初始化时下断点

Person *p = [Person alloc] ;
Person *p1 = [Person alloc];
for (int i = 0; i < 10; i++) {
   NSLog(@"---");
   dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (p) {
            NSLog(@"Person --- %@, %d", [NSThread currentThread], i);
          	@synchronized (p1) {
                NSLog(@"Person --- %@, %d", [NSThread currentThread], i);
            }
         }
    });
}

配合着调用栈的线程来观察,因为是开了多条线程 image.png 我们发现在初次进来的时候这个data 并没有值,在240行走完之后,再p以下 image.png 这个就是创建的第一个SyncData,我们看下第二个创建的时候,有没有冲突, 等到第二次的点过来的时候 image.png 我们知道拉链法是采用的头插法插入的 image.png 这里result->nextData刚好指向了第一个SyncData。在同一个线程空间里,对象不是生成一整个拉链,当产生哈希冲突的时候就会有拉链的生成。上面的例子只有一个SyncList的时候,p1和p2都会在【0】这条拉链里面,每一个SyncData里面都会有一个标记threadCount来表明被多少条线程锁。 ​

锁的归类

⾃旋锁:线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏,因此是⼀种忙等待。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释放⾃旋锁。⾃旋锁避免了进程上下⽂的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。有互斥和同步的概念,多条线程同时处理的时候,按照响应的顺序来处理。

互斥锁:是⼀种⽤于多线程编程中,防⽌两条线程同时对同⼀公共资源(⽐如全局变量)进⾏读写的机制。该⽬的通过将代码切⽚成⼀个⼀个的临界区⽽达成 ​

互斥锁有:NSLock pthread_mutex @synchronized

PosixThread中定义有⼀套专⻔⽤于线程同步的mutex函数,⽤于保证在任何时刻,都只能有⼀个线程访问该对象。当获取锁操作失败时,线程会进⼊睡眠,等待锁释放时被唤醒 ​

1.创建和销毁

  • A:POSIX定义了⼀个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁
  • B:intpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*mutexattr)
  • C:pthread_mutex_destroy()⽤于注销⼀个互斥锁

2.锁操作

  • intpthread_mutex_lock(pthread_mutex_t*mutex)
  • intpthread_mutex_unlock(pthread_mutex_t*mutex)
  • intpthread_mutex_trylock(pthread_mutex_t*mutex) pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY⽽不是挂起等待 ​

NSLock

- (void)test1 {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
            }
        };
        testMethod(10);
    });
}

image.png 如果我们此时在外层嵌套一个for循环呢

- (void)test1 {
    for (int i= 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            testMethod = ^(int value){
                if (value > 0) {
                    NSLog(@"current value = %d",value);
                    testMethod(value - 1);
                }
            };
            testMethod(10);
        });
    }
}

image.png 我们可以发现,此时数据就出现了问题。此时我们就需要加一把锁,此时打印输出就没有问题了。

[lock lock];
testMethod(10);
[lock unlock];

如果我们换个地方加锁呢? image.png 此时在47行加锁的时候,还没有unlock解锁的时候就再次调用testMethod,造成了一直加锁一直加锁成了死锁。从这个方向也验证了当前的NSLock这把锁不可递归。

NSRecursiveLock

我们在上面的lock和unlock处使用NSRecurisiveLock来加锁 image.png 也会崩溃也只打印了一遍,我这里明明外层还有个循环,所以说NSRecursiveLock可递归但是不是多线程的。只是解决了递归性,如果使用@synchronized则完美的解决了这些问题。 ​

NSCondition

NSCondition的对象实际上作为⼀个锁和⼀个线程检查器:锁主要为了当检测条件时保护数据源,执⾏条件引发的任务;线程检查器主要是根据条件决定是否继续运⾏线程,即线程是否被阻塞 ​

  • 1:[condition lock]// ⼀般⽤于多线程同时访问、修改同⼀个数据源,保证在同⼀时间内数据源只被访问、修改⼀次,其他线程的命令需要在lock外等待,只到unlock,才可访问
  • 2:[condition unlock];// 与lock同时使⽤
  • 3:[condition wait];// 让当前线程处于等待状态
  • 4:[condition signal];// CPU发信号告诉线程不⽤在等待,可以继续执⾏ ​

比较典型的是生产消费者模式。可以多线程生产,但是消费的时候必须要保证生产数量 > 0,如果消费的时候库存等于0 ,就等待。

- (void)producer{
    [_testCondition lock]; // 操作的多线程影响
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal]; // 信号
    [_testCondition unlock];
}

- (void)consumer{
     [_testCondition lock];  // 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}

NSLockNSRecursiveLockNSCondition都来源于底层Foundation框架。 ​

Foundation源码关于锁的封装

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

实际上这个锁是一个协议,遵守NSLocking协议的锁都要实现lockunlock两个方法。在Foundation源码中NSLock的初始化调用的是pthread_mutex_init方法 NSLock的相关定义:

 open class NSLock: NSObject, NSLocking {   
	public override init() {
        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() {
        pthread_mutex_lock(mutex)
    }
 }

NSRecursiveLock的相关定义:

open class NSRecursiveLock: NSObject, NSLocking {   
    public override init() {
        super.init()
#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() {
        pthread_mutex_lock(mutex)
    }
 }

经过两个对比,可以发现递归锁NSRecursiveLock多了几行代码PTHREAD_MUTEX_RECURSIVE这里就是设置可递归的地方。 ​

NSConditionLock

- (void)testConditonLock{
    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];
    });
}

执行顺序:这里的执行顺序一定是2比1先执行。 image.png 疑问:

  • 1: NSConditionLock VS NSCondition
  • 2: 2 -> 是什么东西
  • 3: lockWhenCondition -> 如何控制
  • 4: unlockWithCondition 又做了什么?

-[NSConditionLock initWithCondition:]分析

带着上面那几个疑问,我们使用真机定位一下源码看看 image.png 这里也再次验证了上面提到了定位到Foundation框架的原因。真机的话,我们知道x0,x1和x2是参数 image.png 我们在汇编里面主要看bl跳转相关的一些处理,我们把所有的bl都断点一下行数分别在21,23,30,35,37,52 image.png 可以得出有个对象调用了[init]带了一个参数2,依次循环LLDB输出,大致得出方法调用顺序:

-[NSConditionLock initWithCondition:]

  1. [? init: 2]
  2. [NSConditionLock init: 2]
  3. [NSConditionLock zone]
  4. [NSCondition allocWithZone]
  5. [NSCondtion init]

接着继续就走到了52行的retain方法,我们知道返回值一般放在x0里面,所以我们输出一下 image.png 而上面已经分析出来的方法可以猜测到,前面5步已经开辟了内存空间,那么此时我们可以使用x/4gx分析,得出结论,在NSConditionLock内部有一个成员变量NSCondition,外部传进来的值也保存进来了。

-[NSConditionLock lockWhenCondition:]分析

image.png 此时在这里的12,24行也加上断点,跟下跳转的流程,慢慢调试,得出下面的方法。

-[NSConditionLock lockWhenCondition:]

  1. [NSDate distantFuture]
  2. [NSConditionLock lockWhenCondition:beforeDate:]
    1. [NSCondition lock]
    2. [NSCondition waitUntilData] 此时我们可以在方法[NSConditionLock lockWhenCondition:beforeDate:]加一个符号断点,跟下这里面的方法。最终得到了以上的步骤,用相同的方法跟踪下[NSConditionLock lockWhenCondition:]的流程

-[NSConditionLock lockWhenCondition:]

  1. [NSCondition lock]
  2. [NSCondition broadcast]
  3. [NSCondition unlock]

​ 最直观的方法,我们可以直接打开Foundation的源码,但是上面的流程也不是没有用处,万一源码不公开呢?上面的是分析的常规思路。