版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.05.20 |
前言
ios中有好几种锁,比如自旋锁,互斥锁,信号量等等,锁其实是多线程数据安全的一种解决方案,作用就是保证同一时间只有一个线程访问和改变某些敏感数据,这些锁的性能也是差别很大,最近看了几个技术大牛的技术博客,我才发现我以前对锁的理解太肤浅了,心虚的赶紧找资料又开始了深入学习,然后整理出来。
这篇主要讲这几种锁的基本情况。
详情
在说明几种锁的基本情况之前,我们先看看ios开发中这八种锁的名称和它们的性能,如下图所示。
下面主要对这几种锁的使用简单的进行说明。
一、OSSpinLock自旋锁
OSSpinLock自旋锁,它的性能相对是最高的,差不多是150us。下面我们直接上代码。
1. JJOSSLockVC.h
#import
@interface JJOSSLockVC : UIViewController
@end
2. JJOSSLockVC.m
#import "JJOSSLockVC.h"
#import "libkern/OSAtomic.h"
@interface JJOSSLockVC ()
@end
@implementation JJOSSLockVC
#pragma mark - Override Base Function
- (void)viewDidLoad
{
[super viewDidLoad];
//自旋锁
[self aboutOssPinLock];
}
#pragma mark - Object Private Function
//自旋锁
- (void)aboutOssPinLock
{
__block OSSpinLock osslock = OS_SPINLOCK_INIT;
NSInteger __block num = 10;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程1%@准备上锁",[NSThread currentThread]);
OSSpinLockLock(&osslock);
NSLog(@"我是线程1%@",[NSThread currentThread]);
num = num + 1;
NSLog(@"num1=%ld",num);
OSSpinLockUnlock(&osslock);
NSLog(@"我是线程1解锁了");
});
NSLog(@"---------分割线----------");
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程2%@准备上锁",[NSThread currentThread]);
OSSpinLockLock(&osslock);
NSLog(@"我是线程2%@",[NSThread currentThread]);
num = num - 1;
NSLog(@"num2=%ld",num);
OSSpinLockUnlock(&osslock);
NSLog(@"我是线程2解锁了");
});
}
@end
看输出结果
2017-05-20 10:05:35.738 lock[1557:65798] 线程1{number = 3, name = (null)}准备上锁
2017-05-20 10:05:35.738 lock[1557:65630] ---------分割线----------
2017-05-20 10:05:35.739 lock[1557:65798] 我是线程1{number = 3, name = (null)}
2017-05-20 10:05:35.739 lock[1557:65799] 线程2{number = 4, name = (null)}准备上锁
2017-05-20 10:05:35.739 lock[1557:65798] num1=11
2017-05-20 10:05:35.740 lock[1557:65798] 我是线程1解锁了
2017-05-20 10:05:35.740 lock[1557:65799] 我是线程2{number = 4, name = (null)}
2017-05-20 10:05:35.740 lock[1557:65799] num2=10
2017-05-20 10:05:35.740 lock[1557:65799] 我是线程2解锁了
由输出结果可知,num数据被锁住了,不会因为两个线程的访问而导致数据不安全。可以发现当我们同时锁上线程1和线程2的时候,线程2会一直等待(自旋锁不会让等待的进入睡眠状态),直到线程1的任务执行完且解锁完毕,线程2才会执行。
下面我们修改一下代码,将线程1的解锁代码注释掉
//注释掉线程1的解锁代码
// OSSpinLockUnlock(&osslock);
让我们看一下输出结果
2017-05-20 10:27:56.603 lock[1832:82635] ---------分割线----------
2017-05-20 10:27:56.603 lock[1832:82692] 线程1{number = 3, name = (null)}准备上锁
2017-05-20 10:27:56.604 lock[1832:82692] 我是线程1{number = 3, name = (null)}
2017-05-20 10:27:56.604 lock[1832:82693] 线程2{number = 4, name = (null)}准备上锁
2017-05-20 10:27:56.604 lock[1832:82692] num1=11
2017-05-20 10:27:56.604 lock[1832:82692] 我是线程1解锁了
由输出结果可知,因为我们注释掉了线程1中的解锁代码,会绕过线程1,直到调用了线程2的解锁方法才会继续执行线程1中的任务,正常情况下,lock和unlock最好成对出现。这里注释掉了线程1的解锁代码,导致线程1无法解锁,所以线程2里面的num2不会执行和打印输出。
这里面用到了几个参数,如下所示:
OS_SPINLOCK_INIT: 默认值为 0,在 locked 状态时就会大于 0,unlocked状态下为 0
OSSpinLockLock(&oslock):上锁,参数为 OSSpinLock 地址
OSSpinLockUnlock(&oslock):解锁,参数为 OSSpinLock 地址
OSSpinLockTry(&oslock):尝试加锁,可以加锁则立即加锁并返回 YES,反之返回 NO
这里还要说一下trylock和lock的区别,如下所示:
- 当前线程锁失败,也可以继续其它任务,用 trylock 合适。
- 当前线程只有锁成功后,才会做一些有意义的工作,那就 lock,没必要轮询 trylock。
二、dispatch_semaphore 信号量
还是直接看代码
1. JJSemaphoreLockVC.h
#import
@interface JJSemaphoreLockVC : UIViewController
@end
2. JJSemaphoreLockVC.m
#import "JJSemaphoreLockVC.h"
@interface JJSemaphoreLockVC ()
@end
#pragma mark - Override Base Function
@implementation JJSemaphoreLockVC
- (void)viewDidLoad
{
[super viewDidLoad];
[self aboutSemaphoreLock];
}
#pragma mark - Object Private Function
- (void)aboutSemaphoreLock
{
//传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
NSInteger __block num = 10;
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"我是线程1%@,等待中",[NSThread currentThread]);
//signal值 -1
dispatch_semaphore_wait(signal, overTime);
num = num + 1;
NSLog(@"num1=%ld",num);
//signal值 +1
dispatch_semaphore_signal(signal);
NSLog(@"线程1 发送信号");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"我是线程2%@,等待中",[NSThread currentThread]);
//signal值 -1
dispatch_semaphore_wait(signal, overTime);
num = num + 1;
NSLog(@"num2=%ld",num);
//signal值 +1
dispatch_semaphore_signal(signal);
NSLog(@"线程2 发送信号");
});
}
@end
下面看输出结果
2017-05-20 11:02:49.473 lock[2238:107199] 我是线程2{number = 4, name = (null)},等待中
2017-05-20 11:02:49.473 lock[2238:107200] 我是线程1{number = 3, name = (null)},等待中
2017-05-20 11:02:49.474 lock[2238:107199] num2=11
2017-05-20 11:02:49.476 lock[2238:107199] 线程2 发送信号
2017-05-20 11:02:49.476 lock[2238:107200] num1=12
2017-05-20 11:02:49.478 lock[2238:107200] 线程1 发送信号
由上发现,因为我们初始化信号量的时候是大于 0 的,所以并没有阻塞线程,而是直接执行了线程1和线程2。
下面说一下信号量的几个参数
dispatch_semaphore_create(1): 传入值必须 >=0, 若传入为 0 则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_semaphore_wait(signal, overTime):可以理解为 lock,会使得 signal 值 -1
dispatch_semaphore_signal(signal):可以理解为 unlock,会使得 signal 值 +1
下面有个比较形象的比喻,是我在别的博客上看到的,写的不错。
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值(signal): 它就相当于剩余车位的数目,dispatch_semaphore_wait 函数就相当于来了一辆车,dispatch_semaphore_signal 就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次 dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait 剩余车位就减少一个;当剩余车位为 0 时,再来车(即调用 dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
我们再次修改代码
//dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_semaphore_t signal = dispatch_semaphore_create(0);
我们在看一下输出结果
2017-05-20 11:14:49.192 lock[2422:116184] 我是线程2{number = 4, name = (null)},等待中
2017-05-20 11:14:49.192 lock[2422:116201] 我是线程1{number = 3, name = (null)},等待中
2017-05-20 11:14:52.267 lock[2422:116201] num1=11
2017-05-20 11:14:52.267 lock[2422:116184] num2=12
2017-05-20 11:14:52.267 lock[2422:116201] 线程1 发送信号
2017-05-20 11:14:52.268 lock[2422:116184] 线程2 发送信号
这个主要是看时间戳,可以看见49~52,也就是说dispatch_semaphore_create(0)时,线程1和2里面的代码输出num1和num2的值要等3s才会执行,而不会立即执行。
相关参考技术博客
1.iOS 开发中的八种锁(Lock)
2.不再安全的 OSSpinLock
3. NSRecursiveLock递归锁的使用
4.关于dispatch_semaphore的使用
5.实现锁的多种方式和锁的高级用法
后记
今天就写了2种锁,剩下的待续~~~