美文网首页
iOS底层原理 - 八大锁分析

iOS底层原理 - 八大锁分析

作者: 孜孜不倦_闲 | 来源:发表于2020-07-28 14:55 被阅读0次

    回顾之前

    前文讲到多线程原理,线程安全、线程阻塞、线程使用等;这节我们来分析一下有关线程安全的一部分:锁,线程锁。

    锁初识

    我们所用到的锁,是为了解决线程安全问题;一段代码段在同一个时间只能允许被有限个线程访问,解决资源竞争导致的数据紊乱;八大线程锁即:1.NSClock,2.NSConditionLock,3.NSCondition,4.NSRecursiveLock,5.@synchronized,6.dispatch_semaphore,7.OSSpinLock,8.pthread_mutex

    线程资源竞争问题举例:

    @property (nonatomic, assign) NSInteger totalNum;
    
    -(void)viewDidLoad {
    
        _totalNum = 10;
    
        [self threadSecuretTest];
    
    }
    
       
    
    - (void)runMoreTickets{
    
        if (_totalNum == 0) {
    
            return;
    
        }
    
        sleep(0.2);
    
        NSLog(@"%ld (currentThreadName:%@, currentThreadCount:%ld)",_totalNum --, [NSThread currentThread].name,[NSThread currentThread].stackSize);
    
    }
    
    - (void)threadSecuretTest {
    
        dispatch_queue_t queuet = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        dispatch_async(queuet, ^{
    
            for (int i = 0; i < 5; i++) {
    
                [self runMoreTickets];
    
            }
    
        });
    
        
    
        dispatch_async(queuet, ^{
    
            for (int i = 0; i < 5; i++) {
    
                [self runMoreTickets];
    
            }
    
        });
    
        dispatch_async(queuet, ^{
    
            for (int i = 0; i < 5; i++) {
    
                [self runMoreTickets];
    
            }
    
        });
    
    }
    
    结果:** 2020-07-20 09:47:15.702720+0800 Test-OC[86022:6943874] 8 (currentThreadName:, currentThreadCount:524288)**
    
    2020-07-20 09:47:15.702720+0800 Test-OC[86022:6943879] 10 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.702720+0800 Test-OC[86022:6943873] 9 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.702897+0800 Test-OC[86022:6943874] 6 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.702898+0800 Test-OC[86022:6943879] 7 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.702897+0800 Test-OC[86022:6943873] 6 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.703034+0800 Test-OC[86022:6943879] 4 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.703028+0800 Test-OC[86022:6943874] 5 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.703057+0800 Test-OC[86022:6943873] 3 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.703122+0800 Test-OC[86022:6943879] 2 (currentThreadName:, currentThreadCount:524288)
    
    2020-07-20 09:47:15.703385+0800 Test-OC[86022:6943874] 1 (currentThreadName:, currentThreadCount:524288)
    

    锁详解

    1.NSClock

      互斥锁,加锁过程是按照队列的形式(FIFO),先进先出的原则。
    
    @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
    

    NSLock遵循NSLocking协议。lock是加锁,unLock是解锁,tryLock是尝试加锁,失败的话会返回NO;lockBeforeDate:是在指定时间前尝试加锁,要是在指定时间前加锁失败则返回NO。

    看个例子
    
    // 主线程
    
        NSLock * lock = [[NSLock alloc] init];
    
        dispatch_queue_t group = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        // 1
    
        dispatch_async(group, ^{
    
            [lock lock];
    
            sleep(3);
    
            NSLog(@"线程1");
    
            [lock unlock];
    
            NSLog(@"线程1解锁成功");
    
        });
    
        // 2
    
        dispatch_async(group, ^{
    
            sleep(2);
    
            [lock lock];
    
            NSLog(@"线程2");
    
            [lock unlock];
    
        });
    
    2020-07-20 10:23:55.627833+0800 Test-OC[86238:6963970] 线程1
    
    2020-07-20 10:23:55.628053+0800 Test-OC[86238:6963970] 线程1解锁成功
    
    2020-07-20 10:23:55.628061+0800 Test-OC[86238:6963971] 线程2 
    

    由上面打印结果可以看出,线程1加锁时阻塞了线程2,线程二加锁失败。3s后线程1解锁了,线程二才加锁成功;

      建议使用tryLock ,尝试加锁,成功返回YES,再使用解锁,这样不会阻塞线程。
    
      如果是三个线程,那么一个线程在加锁的时候,其余请求锁的线程将形成一个等待队列,按先进先出原则,这个结果可以通过修改线程优先级进行测试得出。
    

    2.NSConditionLock

    和NSLock类似,都遵循NSLocking协议,不过多了condition属性。

    @interface NSConditionLock : NSObject <NSLocking> {
    
    @private
    
        void *_priv;
    
    }
    
    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
    
    @property (readonly) NSInteger condition;
    
    - (void)lockWhenCondition:(NSInteger)condition;
    
    - (BOOL)tryLock;
    
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    
    - (void)unlockWithCondition:(NSInteger)condition;
    
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    举个栗子:

    NSConditionLock * conditionLock = [[NSConditionLock alloc] initWithCondition:0];
    
        dispatch_queue_t group = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        // 1
    
        dispatch_async(group, ^{
    
            [conditionLock lockWhenCondition:1];
    
            NSLog(@"线程1");
    
            sleep(2);
    
            [conditionLock unlock];
    
        });
    
        
    
        // 2
    
        dispatch_async(group, ^{
    
            sleep(1);
    
            if ([conditionLock tryLockWhenCondition:0]) {
    
                NSLog(@"线程2");
    
                [conditionLock unlockWithCondition:2];
    
                NSLog(@"线程2解锁成功");
    
            }else{
    
                NSLog(@"线程2尝试加锁失败");
    
            }
    
        });
    
        // 3
    
        dispatch_async(group, ^{
    
            sleep(2);
    
            if ([conditionLock tryLockWhenCondition:2]) {
    
                NSLog(@"线程3");
    
                [conditionLock unlock];
    
                NSLog(@"线程3解锁成功");
    
            }else{
    
                NSLog(@"线程3尝试加锁失败");
    
            }
    
        });
    
        // 4
    
        dispatch_async(group, ^{
    
            sleep(3);
    
            if ([conditionLock tryLockWhenCondition:2]) {
    
                NSLog(@"线程4");
    
                [conditionLock unlockWithCondition:1];
    
                NSLog(@"线程4解锁成功");
    
            }else{
    
                NSLog(@"线程4尝试加锁失败");
    
            }
    
        });
    
    2020-07-20 13:53:10.877963+0800 Test-OC[88096:7132297] 线程2
    
    2020-07-20 13:53:10.878170+0800 Test-OC[88096:7132297] 线程2解锁成功
    
    2020-07-20 13:53:11.876597+0800 Test-OC[88096:7132296] 线程3
    
    2020-07-20 13:53:11.876787+0800 Test-OC[88096:7132296] 线程3解锁成功
    
    2020-07-20 13:53:12.878735+0800 Test-OC[88096:7132295] 线程4
    
    2020-07-20 13:53:12.878914+0800 Test-OC[88096:7132295] 线程4解锁成功
    
    2020-07-20 13:53:12.878923+0800 Test-OC[88096:7132298] 线程1
    

    上述代码先输出线程2而没先输出线程1,由于condition1开始处于未满足处于解锁状态,会使线程1处于waiting状态,到线程4解锁出1时才满足条件;tryLockWhenCondition 即使未满足条件也不会返回NO,不会阻塞当前线程。

    可以看出,NSConditionLock还可实现任务依赖关系

    3.NSCondition

    NScondition的实例化对象作为锁和锁的检查器使用;不像其他锁一样,先轮询,而是直接进入waiting状态当有其他 线程执行signal或者broadcast方法时,线程被唤醒,继续之后的方法;锁上之后其他线程仍然可以上锁,之后可以根据条件判断是否继续运行线程,即线程是否进入waiting状态。

    @interface NSCondition : NSObject <NSLocking> {
    
    @private
    
        void *_priv;
    
    }
    
    - (void)wait;
    
    - (BOOL)waitUntilDate:(NSDate *)limit;
    
    - (void)signal;
    
    - (void)broadcast;
    
    @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    
    @end
    

    举例用法:

    NSCondition * condition = [[NSCondition alloc] init];
    
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        NSMutableArray *testArr = [NSMutableArray array];
    
        dispatch_async(queue, ^{
    
            [condition lock];
    
            while (!testArr.count) {
    
                [condition wait];
    
            }
    
            NSLog(@"线程1");
    
            [testArr removeAllObjects];
    
            NSLog(@"testArr removeAllObject");
    
            [condition unlock];
    
        });
    
        
    
        dispatch_async(queue, ^{
    
            [condition lock];
    
            [testArr addObject:@1];
    
            NSLog(@"线程2, textArrAddObject");
    
            [condition signal];
    
            [condition unlock];
    
        });
    
    2020-07-20 14:39:39.001432+0800 Test-OC[88547:7161017] 线程2, textArrAddObject
    
    2020-07-20 14:39:39.001750+0800 Test-OC[88547:7161016] 线程1
    
    2020-07-20 14:39:39.001881+0800 Test-OC[88547:7161016] testArr removeAllObject
    

    上面结果可以看出,condition上锁后还是可以继续上锁,并不会阻塞线程;使用场景更多在锁定条件对象,测试是否安全的执行以下任务。

    其中signal 和broadcast 方法区别在于,signal是信号量控制,调用一次只能唤起一次线程等待;想要唤醒多次需要多次调用;broadcast则是可以唤醒所有线程等待。若无线程等待,调用两方法都无作用。

    4.NSRecursiveLock

    递归锁,他与NSLock区别在于,NSRecursiveLock可以在一个线程里重复加锁(由于单线程是按顺序执行,不会出现资源竞争的情况);NSRecursiveLock会记住上锁和解锁的次数,只有平衡了才会释放锁。其他线程才能上锁成功

    **@interface** NSRecursiveLock : 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**
    

    使用举例:

    
    NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
    
    ​ dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    ​
    
    ​ dispatch_async(queue, ^{
    
    ​ **static**  **void** (^RecursiveLockBlock)(**int**);
    
    ​ RecursiveLockBlock = ^(**int** value){
    
    ​ [lock lock];
    
    ​ **if** (value > 0) {
    
    ​ NSLog(@"Value: %d",value);
    
    ​ RecursiveLockBlock(value - 1);
    
    ​ }
    
    ​ };
    
    ​ RecursiveLockBlock(3);
    
    ​ });
    
    **2020-07-20 15:58:38.965564+0800 Test-OC[89057:7199031] Value: 3**
    
    **2020-07-20 15:58:38.965825+0800 Test-OC[89057:7199031] Value: 2**
    
    **2020-07-20 15:58:38.965924+0800 Test-OC[89057:7199031] Value: 1**
    
    

    从上可知,使用NSRecursiveLock 在加锁未解锁情况下重复加锁而不会阻塞线程,要是替换成NSLock加锁未解锁继续加锁就会阻塞线程,下面代码就不会执行了;递归锁就是为了解决这种问题。

    5.@sychronized

    ​ 对象级别锁,互斥锁。@sychronized(object) ,只有object相同情况下才满足互斥条件。

    ​ 举个🌰

    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    ​ dispatch_async(queue, ^{
    
    ​ **@synchronized** (**self**) {
    
    ​ NSLog(@"线程1");
    
    ​ }
    
    ​ });
    
    ​ dispatch_async(queue, ^{
    
    ​ sleep(2);
    
    ​ **@synchronized** (**self**) {
    
    ​ NSLog(@"线程2");
    
    ​ }
    
    ​ });
    
    

    @synchronized (object) 用法比较简单,在方法内已经处理好加解锁;不过性能相对是较差的那种;

    6.dispatch_semaphore

    ​ 信号量,控制线程最大并发数,也是锁的一种使用

    dispatch_semaphore_t

    // 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句

    dispatch_semaphore_create(long value);

    // 可以理解为 lock,会使得 signal-1

    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

    可以理解为 unlock,会使得 signal+1

    dispatch_semaphore_signal(dispatch_semaphore_t dsema);

    使用举例:

    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 2);
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(queue, ^{
    
    NSLog(@"线程1 线程等待");
    
    dispatch_semaphore_wait(semaphore, overTime);
    
    NSLog(@"线程1");
    
    dispatch_semaphore_signal(semaphore);
    
    NSLog(@"线程1 signal发信号");
    
    });
    
    dispatch_async(queue, ^{
    
    NSLog(@"线程2 线程等待");
    
    dispatch_semaphore_wait(semaphore, overTime);
    
    NSLog(@"线程2");
    
    dispatch_semaphore_signal(semaphore);
    
    NSLog(@"线程2 signal发信号");
    
    });
    
    **2020-07-23 16:39:02.668485+0800 Test-OC[2783:214582] 线程2 线程等待**
    
    **2020-07-23 16:39:02.668485+0800 Test-OC[2783:214584] 线程1 线程等待**
    
    **2020-07-23 16:39:02.668794+0800 Test-OC[2783:214584] 线程1**
    
    **2020-07-23 16:39:02.668812+0800 Test-OC[2783:214582] 线程2**
    
    **2020-07-23 16:39:02.668920+0800 Test-OC[2783:214584] 线程1 signal发信号**
    
    **2020-07-23 16:39:02.668911+0800 Test-OC[2783:214582] 线程2 signal发信号**
    
    

    从上述结果可以看出,在线程等待后才开始后续的方法执行;类似于教室座位计算,人满了外面会处于等待状态,有座位才安排;设置dispatch_semaphore_create(0) 为0 时overTime生效,等待满足实际才开始执行下面的任务;

    7.OSSPinkLock

    自旋锁,效率为最高(iOS10之后被官方认定为不安全的锁,不建议使用)自旋锁不会让等待的进入睡眠状态

    需导入头文件

    // #import <libkern/OSAtomic.h>

    使用举例:

    
    **__block** OSSpinLock osLock = OS_SPINLOCK_INIT;
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(queue, ^{
    
    NSLog(@"线程1 准备上锁");
    
    OSSpinLockLock(&osLock);
    
    NSLog(@"线程1");
    
    OSSpinLockUnlock(&osLock);
    
    NSLog(@"线程1 解锁");
    
    });
    
    dispatch_async(queue, ^{
    
    NSLog(@"线程2 准备上锁");
    
    OSSpinLockLock(&osLock);
    
    NSLog(@"线程2");
    
    OSSpinLockUnlock(&osLock);
    
    NSLog(@"线程2 解锁");
    
    });
    
    **2020-07-26 23:48:49.448361+0800 Test-OC[7829:456571] 线程1 准备上锁**
    
    **2020-07-26 23:48:49.448361+0800 Test-OC[7829:456570] 线程2 准备上锁**
    
    **2020-07-26 23:48:49.448656+0800 Test-OC[7829:456570] 线程2**
    
    **2020-07-26 23:48:49.448770+0800 Test-OC[7829:456570] 线程2 解锁**
    
    **2020-07-26 23:48:49.448951+0800 Test-OC[7829:456571] 线程1**
    
    **2020-07-26 23:48:49.449080+0800 Test-OC[7829:456571] 线程1 解锁**
    
    

    从运行结果可以看出我们锁住线程1和线程二时,线程二会一直处于线程等待状态,直到线程1解锁完成,线程二会立即执行;

    如果我们改变一些解锁状态看一下运行是否受影响

    
    /// 将线程一解锁关闭
    
    dispatch_async(queue, ^{
    
    NSLog(@"线程1 准备上锁");
    
    OSSpinLockLock(&osLock);
    
    NSLog(@"线程1");
    
    // OSSpinLockUnlock(&osLock);
    
    NSLog(@"线程1 解锁");
    
    }); ....
    
    **2020-07-27 00:03:05.802912+0800 Test-OC[8003:465228] 线程1 准备上锁**
    
    **2020-07-27 00:03:05.802912+0800 Test-OC[8003:465230] 线程2 准备上锁**
    
    **2020-07-27 00:03:05.803140+0800 Test-OC[8003:465228] 线程1**
    
    **2020-07-27 00:03:05.803245+0800 Test-OC[8003:465228] 线程1 解锁**
    
    

    可以看出线程1未解锁线程2也不会执行,所以oslocklockunlock需要成对存在

    OS_SPINLOCK_INIT: 默认值为 0,在 locked 状态时就会大于 0unlocked状态下为 0

    OSSpinLockLock(&oslock):上锁,参数为 OSSpinLock 地址

    OSSpinLockUnlock(&oslock):解锁,参数为 OSSpinLock 地址

    OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO

    8.pthread_mutex

    递归锁, ibireme在《不再安全的 OSSpinLock》文章中提到,性能最好的OSSPinkLock不再安全已将osspinklock替换为pthread_mutex

    需提前导入头文件:

    // #import "pthread.h"

    使用举例:

    static pthread_mutex_t p_lock;
    
    - (void)pthread_mutex_t_Text{
    
        pthread_mutex_init(&p_lock, NULL);
    
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        dispatch_async(queue, ^{
    
            NSLog(@"线程1 准备加锁");
    
            pthread_mutex_lock(&p_lock);
    
            sleep(2);
    
            NSLog(@"线程1");
    
            pthread_mutex_unlock(&p_lock);
    
            NSLog(@"线程1 解锁成功");
    
        });
    
        dispatch_async(queue, ^{
    
            NSLog(@"线程2 准备加锁");
    
            pthread_mutex_lock(&p_lock);
    
            sleep(2);
    
            NSLog(@"线程2");
    
            pthread_mutex_unlock(&p_lock);
    
            NSLog(@"线程2 解锁成功");
    
        });
    
    }
    
    2020-07-27 10:08:05.713354+0800 Test-OC[8616:492609] 线程2 准备加锁
    
    2020-07-27 10:08:05.713354+0800 Test-OC[8616:492610] 线程1 准备加锁
    
    2020-07-27 10:08:07.717438+0800 Test-OC[8616:492610] 线程1
    
    2020-07-27 10:08:07.717653+0800 Test-OC[8616:492610] 线程1 解锁成功
    
    2020-07-27 10:08:09.720069+0800 Test-OC[8616:492609] 线程2
    
    2020-07-27 10:08:09.720386+0800 Test-OC[8616:492609] 线程2 解锁成功
    

    从运行结果可以看出,原理和osspinkLock类似,不过ossPinkLocktry(&lock)pthread_mutex_trylock区别在于后者返回的是0,否则就是一个错误提示码,前者则返回YES/NO

    YYCache中pthread_mutex_t用法:

    YYCache Lock.png
    pthread_mutex_t(recursive)
     pthread_mutex_t pLock;
     pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr); //初始化attr并且给它赋予默认
     pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //设置锁类型,这边是设置为递归锁
     pthread_mutex_init(&pLock, &attr);
    pthread_mutexattr_destroy(&attr); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用</pre>
    
     pthread_mutex还有一种递归锁的用法,从上面用法可知,一般需要加锁之后只能一个线程对象访问,不解锁下个线程是不可访问的。递归锁,可以在一个线程内重复加锁而不释放
    

    使用举例:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
        dispatch_async(queue, ^{
    
            static void (^recursiveBlock)(int);
    
            recursiveBlock = ^(int value){
    
                pthread_mutex_lock(&p_lock);
    
                if (value > 0) {
    
                    NSLog(@"value: %d",value);
    
                    recursiveBlock(value-1);
    
                }
    
                pthread_mutex_unlock(&p_lock);
    
            };
    
            recursiveBlock(7);
    
        });
    
    2020-07-27 10:58:07.534516+0800 Test-OC[9103:517900] value: 7
    
    2020-07-27 10:58:07.534677+0800 Test-OC[9103:517900] value: 6
    
    2020-07-27 10:58:07.534803+0800 Test-OC[9103:517900] value: 5
    
    2020-07-27 10:58:07.534899+0800 Test-OC[9103:517900] value: 4
    
    2020-07-27 10:58:07.534992+0800 Test-OC[9103:517900] value: 3
    
    2020-07-27 10:58:07.535128+0800 Test-OC[9103:517900] value: 2
    
    2020-07-27 10:58:07.535833+0800 Test-OC[9103:517900] value: 1
    

    总结

    图引自ibeme,性能对比.png

    参考:https://www.jianshu.com/p/b3ab3d390903

    相关文章

      网友评论

          本文标题:iOS底层原理 - 八大锁分析

          本文链接:https://www.haomeiwen.com/subject/wkufrktx.html