iOS底层原理 - 八大锁分析

回顾之前

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

锁初识

我们所用到的锁,是为了解决线程安全问题;一段代码段在同一个时间只能允许被有限个线程访问,解决资源竞争导致的数据紊乱;八大线程锁即: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  {
@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  {

@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  {

@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  {

**@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

使用举例:


**__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); //销毁一个属性对象,在重新进行初始化之前该结构不能重新使用
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底层原理 - 八大锁分析)