21、iOS底层分析 - 线程锁(一)NSLock

大纲

  1. 常用锁介绍
  2. 自旋锁和互斥锁的一些问题
  3. NSLock及源码分析
  4. NSLock 坑

一、常用锁介绍

锁的目的是为了解决资源抢夺
    
     iOS中的常用的锁有如下几种:
     1、自旋锁:
        使用与多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直到显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。NSSpinLock ,它现在被废弃了,不能使用了,它是有缺陷的,会造成死锁
     2、互斥锁
        是一种用于多线程编程中,防止两条线程同时对同一公共资源(例如:同一个全局变量)进行读写的机制。互相排斥。例如线程A获取到锁,在释放锁之前,其他线程都获取不到锁。互斥锁也分为两种:递归锁和非递归锁。互斥锁是通过将代码切片成一个一个的临时区来实现。p_thread_mutex,NSLock,@synchronized这个顺序是按照性能排序的,也是我们常用的几个互斥锁。
     3、读写锁:
        计算机程序的并发控制的一种同步机制,也称“共享 - 互斥锁”、多个读者,单个作者(写入)的锁机制。用于解决多线程对公共资源读写问题,读操作可并发重入,写操作时互斥的。读写锁通常用互斥锁、条件变量、信号量实现。
     4、信号量:
        是一种更高级的同步机制,有更多的取值空间。用来实现更加复杂的同步,而不单单是线程间互斥。semphone在一定程度也可以当互斥锁用,它适用于编程逻辑更复杂的场景,同时它也是除了自旋锁以外性能最高的锁
     5、条件锁:
        就是条件变量,当进程的某些资源要求不满足时就锁住进入休眠。当资源被分配到了,条件锁打开继续运行。NSCondition,条件锁我们调用wait方法就把当前线程进入等待状态,当调用了signal方法就可以让该线程继续执行,也可以调用broadcast广播方法。
    
     临时区:
        指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

锁是线程编程同步工具的基础。iOS开发中常用的锁有如下几种:

  1. @synchronized
  2. NSLock 对象锁
  3. NSRecursiveLock 递归锁
  4. NSConditionLock 条件锁
  5. pthread_mutex 互斥锁(C语言)
  6. dispatch_semaphore 信号量实现加锁(GCD)
  7. OSSpinLock (暂不建议使用,原因参见这里

二、自旋锁和互斥锁的一些问题

互斥锁
    互斥锁又分 递归锁(NSRecursiveLock 等)非递归锁(NSLock 等)
    递归锁:可重入锁,统一线程在锁释放前可再次获取锁,即可以递归调用
    非递归锁:不可重入,必须等锁释放后才能再次获取锁。
 
 自旋锁和互斥锁的区别?
    互斥锁:当线程获取锁但没有获取到时,线程进入休眠状态。等到锁被释放,线程会被唤醒同时获取到锁。继续执行任务改变线程状态。
    自旋锁:当线程获取锁没有获取到时,不会进入休眠,而是一直循环看是否可用。线程一直处于活跃状态,不会改变线程状态。

自旋锁和互斥锁的使用场景分别是?
    自旋锁:由于自旋锁一直等待会消耗较多CPU 资源,但是效率较高一旦锁释放立刻就能执行无序唤醒。所以适用于短时间内的轻量级锁定。
    互斥锁:需要修改线程状态,唤醒或休眠线程。所以适用于时间长相对自旋锁效率低的场景。

四、NSLock及源码分析

 NSLock
 非递归 互斥锁。NSLock 互斥锁 不能多次调用 lock方法,会造成死锁
 遵循 NSLocking 协议。进行加锁、解锁

@protocol NSLocking
 
 - (void)lock;//加锁
 - (void)unlock;//解锁
 
 @end
NSLock实现了NSLocking协议:
@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}
// 尝试获取锁,获取到返回YES,获取不到返回NO
- (BOOL)tryLock;
// 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 锁名称,如果使用锁出现异常,输出的log中会有
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end

-tryLock:如果能获取到锁返回YES,如果获取不到锁返回NO,但不会使线程进入休眠,会继续向下执行
-lockBeforeDate::如果锁已被锁定,在指定的时间点之前线程进入休眠等待锁释放。如果在时间点之前锁被释放了,线程立即被唤醒获得锁,该函数会返回YES,继续执行任务,不会一直休眠等到那个时间点。如果等到时间点还没有获得锁会返回NO,并继续执行任务
 NSLock是非递归锁,不能重入,否则会发生死锁:

#import "LJLNSLockViewController.h"

@interface LJLNSLockViewController ()
@property(nonatomic, strong) NSLock *lock;
@end

@implementation LJLNSLockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = [[NSLock alloc] init];
    [NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];   
}

//2020-04-06 13:49:43.713653+0800 filedome[74121:2725361] testLock1
 
- (void)testLock1 {
    [self.lock lock];
    NSLog(@"testLock1");
    [self testLock2];
    [self.lock unlock];
    NSLog(@"testLock1: unlock");
}

- (void)testLock2 {
    [self.lock lock];
    NSLog(@"testLock2");
    [self.lock unlock];
    NSLog(@"testLock2: unlock");
}

可以看到上面的代码最终只打印了testLock1,其他的几个打印不会去执行。因为 testLock1被锁了之后,还没有调用解锁就执行了testLock2。这个时候去lock 但是锁获取不到就休眠等待,直到testLock1 unlock解锁之后才会继续执行,但是这个时候testLock2 不执行完, testLock1 里面的代码也就被卡着不能继续。

注意:

-lock和-unlock必须在相同的线程调用,也就是说,他们必须在同一个线程中成对调用,否则会产生未知结果。

官方文档原文:Unlocking a lock from a different thread can result in undefined behavior.

在不断循环递归,多线程操作的时候。这个时候 _testArray 不断的去初始化新增,release 旧值 如果多条线程访同时敢问到release 那么就会造成多次释放这时候就需要禁止重入的互斥锁。例如:

    NSLock * lock = [[NSLock alloc] init];
    for (int i=0; i<200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            _testArray = [NSMutableArray array];
            [lock unlock];
        });
    }

NSLock 源码

 NSLock是在Foundation中实现的,开源的Foundation是Swift版的:
 源码 目录:demos->003-锁分析->2-Foundation源码

    open func lock() {
        pthread_mutex_lock(mutex)
    }
    
    open func unlock() {
        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
    }
    
    // 对应OC中的 -tryLock
    open func `try`() -> Bool {
        return pthread_mutex_trylock(mutex) == 0
    }
    
    // 对应OC中的 -lockBeforeDate:
    open func lock(before limit: Date) -> Bool {
        if pthread_mutex_trylock(mutex) == 0 {
            return true
        }
        
        return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
    }
    
    private func timedLock(mutex: _MutexPointer, endTime: Date,
                           using timeoutCond: _ConditionVariablePointer,
                           with timeoutMutex: _MutexPointer) -> Bool {
        var timeSpec = timeSpecFrom(date: endTime)
        while var ts = timeSpec {
            let lockval = pthread_mutex_lock(timeoutMutex)
            precondition(lockval == 0)
            let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)
            precondition(waitval == 0 || waitval == ETIMEDOUT)
            let unlockval = pthread_mutex_unlock(timeoutMutex)
            precondition(unlockval == 0)
            
            if waitval == ETIMEDOUT {
                return false
            }
            let tryval = pthread_mutex_trylock(mutex)
            precondition(tryval == 0 || tryval == EBUSY)
            if tryval == 0 { // The lock was obtained.
                return true
            }
            // pthread_cond_timedwait didn't timeout so wait some more.
            timeSpec = timeSpecFrom(date: endTime)
        }
        return false
    }

通过源码可知验证 NSLock 是对 pthread 中互斥锁 的封装。
其他都好理解,这里列一下 timedLock() 的实现流程:
 1、设定超时时间,进入while循环。
 2、pthread_cond_timedwait()在本次循环中计时等待,线程进入休眠
 3、等待超时,直接返回 false;
 4、如果等待没有超时,期间锁被释放,线程会被唤醒,再次尝试获取锁 pthread_mutex_trylock(),如果获取成功返回true
 5、即没有超时,被唤醒后也没有成功获取到锁(被其他线程抢先获得锁),重新计算超时时间进入下一次while循环

五、NSLock 坑

    如下代码。递归调用 testMethod (方法)输出,正常应该是10  9  8 .....,但是实际只输出了10.
    原因分析:因为在if 之前进行锁了之后,在if 里面有递归调用了 testMethod 方法,又一次进来有锁了,这样被锁了多次都没去执行解锁一直处理阻塞。
        这里应该换成递归锁.
        例如:NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];

    NSLock * lock = [[NSLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void(^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value >0) {
                NSLog(@"current value = %d",value);
                testMethod(value-1);
            }
            [lock unlock];
        };
        testMethod(10);
    });


   

发布了104 篇原创文章 · 获赞 13 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/105343281