上篇文章讲了GCD的用法,只要提到了多线程就应该想到线程安全,那么怎么作才能作到在多个线程中保证安全呢? 这篇文章主要讲解线程安全。html
线程安全是什么呢?摘抄一段百度百科的一段话ios
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会经过同步机制保证各个线程均可以正常且正确的执行,不会出现数据污染等意外状况。git
ATM确定用过,你要是边取钱,边存钱,会出问题吗?当你取钱的时候,正在取,结果有人汇款正好到帐,原本1000块取了100剩下900,结果到帐200,1000+200=1200,由于你取的时候,还没取完,汇款到帐告终果数字又加上去了。你取的钱跑哪里去了,这里就须要取钱的时候不能写入数据,就是汇款须要在你取钱完成以后再汇款,不能同时进行。github
那么在iOS中,锁是如何使用的呢?macos
简单从字面上来讲,就是低优先级的任务先于高优先级的任务执行了,优先级搞反了。那在什么状况下会生这种状况呢?编程
假设三个任务准备执行,A,B,C,优先级依次是A>B>C;数组
首先:C处于运行状态,得到CPU正在执行,同时占有了某种资源;sass
其次:A进入就绪状态,由于优先级比C高,因此得到CPU,A转为运行状态;C进入就绪状态;安全
第三:执行过程当中须要使用资源,而这个资源又被等待中的C占有的,因而A进入阻塞状态,C回到运行状态;bash
第四:此时B进入就绪状态,由于优先级比C高,B得到CPU,进入运行状态;C又回到就绪状态;
第五:若是这时又出现B2,B3等任务,他们的优先级比C高,但比A低,那么就会出现高优先级任务的A不能执行,反而低优先级的B,B2,B3等任务能够执行的奇怪现象,而这就是优先反转。
OS_SPINLOCK
叫作自旋锁
,等待锁的进程会处于忙等(busy-wait)状态,一直占用着CPU资源,目前已经不安全,可能会出现优先级翻转问题。
OS_SPINLOCK
API
//初始化 通常是0,或者直接数字0也是ok的。
#define OS_SPINLOCK_INIT 0
//锁的初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁
bool ret = OSSpinLockTry(&lock);
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnlock(&lock);
复制代码
OSSpinLock
简单实现12306如何卖票
//基类实现的卖票
- (void)__saleTicket{
NSInteger oldCount = self.ticketsCount;
if (isLog) {
sleep(sleepTime);
}
oldCount --;
self.ticketsCount = oldCount;
if (isLog) {
printf("还剩% 2ld 张票 - %s \n",(long)oldCount,[NSThread currentThread].description.UTF8String);
}
}
- (void)ticketTest{
self.ticketsCount = 10000;
NSInteger count = self.ticketsCount/3;
dispatch_queue_t queue = dispatch_queue_create("tick.com", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
if (time1 == 0) {
time1 = CFAbsoluteTimeGetCurrent();
}
for (int i = 0; i < count; i ++) {
[self __saleTicket];
}
});
dispatch_async(queue, ^{
if (time1 == 0) {
time1 = CFAbsoluteTimeGetCurrent();
}
for (int i = 0; i < count; i ++) {
[self __saleTicket];
}
});
dispatch_async(queue, ^{
if (time1 == 0) {
time1 = CFAbsoluteTimeGetCurrent();
}
for (int i = 0; i < count; i ++) {
[self __saleTicket];
}
});
dispatch_barrier_async(queue, ^{
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent() - time1;
printf("tick cost time:%f",time);
});
}
- (void)__getMonery{
OSSpinLockLock(&_moneyLock);
[super __getMonery];
OSSpinLockUnlock(&_moneyLock);
}
- (void)__saleTicket{
OSSpinLockLock(&_moneyLock);
[super __saleTicket];
OSSpinLockUnlock(&_moneyLock);
}
- (void)__saveMonery{
OSSpinLockLock(&_moneyLock);
[super __saveMonery];
OSSpinLockUnlock(&_moneyLock);
}
- (void)__saleTicket{
NSInteger oldCount = self.ticketsCount;
oldCount --;
self.ticketsCount = oldCount;
}
//log
还剩 9 张票 - <NSThread: 0x600003dc6080>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x600003dc6080>{number = 3, name = (null)}
还剩 7 张票 - <NSThread: 0x600003dc6080>{number = 3, name = (null)}
还剩 6 张票 - <NSThread: 0x600003df3a00>{number = 4, name = (null)}
还剩 5 张票 - <NSThread: 0x600003df3a00>{number = 4, name = (null)}
还剩 4 张票 - <NSThread: 0x600003df3a00>{number = 4, name = (null)}
还剩 3 张票 - <NSThread: 0x600003dc0000>{number = 5, name = (null)}
还剩 2 张票 - <NSThread: 0x600003dc0000>{number = 5, name = (null)}
还剩 1 张票 - <NSThread: 0x600003dc0000>{number = 5, name = (null)}
复制代码
for (NSInteger i = 0; i < 5; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
}
而后将睡眠时间设置为600s,方便咱们调试。
- (void)__saleTicket{
OSSpinLockLock(&_moneyLock);//此行打断点
[super __saleTicket];
OSSpinLockUnlock(&_moneyLock);
}
复制代码
到了断点进入Debug->Debug WorkFlow ->Always Show Disassembly
,到了汇编界面,在LLDB
输入stepi
,而后一直按enter
,一直重复执行上句命令,直到进入了循环,就是相似下列的三行,发现ja
跳转到地址0x103f3d0f9
,每次执行到ja
老是跳转到0x103f3d0f9
,直到线程睡眠结束。
-> 0x103f3d0f9 <+241>: movq %rcx, (%r8)
0x103f3d0fc <+244>: addq $0x8, %r8
0x103f3d100 <+248>: cmpq %r8, %r9
0x103f3d103 <+251>: ja 0x103f3d0f9
复制代码
能够经过汇编分析了解到自旋锁
是真的忙等
,闲不住的锁。
os_unfair_lock
被系统定义为低级锁,通常低级锁都是闲的时候在睡眠,在等待的时候被内核唤醒,目的是替换已弃用的OSSpinLock
,并且必须使用OS_UNFAIR_LOCK_INIT
来初始化,加锁和解锁必须在相同的线程,不然会中断进程,使用该锁须要系统在__IOS_AVAILABLE(10.0)
,锁的数据结构是一个结构体
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
复制代码
os_unfair_lock
使用很是简单,只须要在任务前加锁,任务后解锁便可。
@interface FYOSUnfairLockDemo : FYBaseDemo
@property (nonatomic,assign) os_unfair_lock lock;
@end
@implementation FYOSUnfairLockDemo
- (instancetype)init{
if (self = [super init]) {
self.lock = OS_UNFAIR_LOCK_INIT;
}
return self;
}
- (void)__saveMonery{
os_unfair_lock_lock(&_unlock);
[super __saveMonery];
os_unfair_lock_unlock(&_unlock);
}
- (void)__getMonery{
os_unfair_lock_lock(&_unlock);
[super __getMonery];
os_unfair_lock_unlock(&_unlock);
}
- (void)__saleTicket{
os_unfair_lock_lock(&_unlock);
[super __saleTicket];
os_unfair_lock_unlock(&_unlock);
}
@end
//log
还剩 9 张票 - <NSThread: 0x600002eb4bc0>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x600002eb4bc0>{number = 3, name = (null)}
还剩 7 张票 - <NSThread: 0x600002eb4bc0>{number = 3, name = (null)}
还剩 6 张票 - <NSThread: 0x600002eb1500>{number = 4, name = (null)}
还剩 5 张票 - <NSThread: 0x600002eb1500>{number = 4, name = (null)}
还剩 4 张票 - <NSThread: 0x600002eb1500>{number = 4, name = (null)}
还剩 3 张票 - <NSThread: 0x600002ed4340>{number = 5, name = (null)}
还剩 2 张票 - <NSThread: 0x600002ed4340>{number = 5, name = (null)}
还剩 1 张票 - <NSThread: 0x600002ed4340>{number = 5, name = (null)}
复制代码
LLDB
中命令stepi
遇到函数会进入到函数,nexti
会跳过函数。咱们将断点打到添加锁的位置
- (void)__saleTicket{
os_unfair_lock_lock(&_unlock);//断点位置
[super __saleTicket];
os_unfair_lock_unlock(&_unlock);
}
复制代码
执行si
,一直enter
,最终是中止该位子,模拟器缺跳出来了,再enter
也没用了,由于线程在睡眠了。syscall
是调用系统函数的命令。
libsystem_kernel.dylib`__ulock_wait:
0x107a3b9d4 <+0>: movl $0x2000203, %eax ; imm = 0x2000203
0x107a3b9d9 <+5>: movq %rcx, %r10
-> 0x107a3b9dc <+8>: syscall
复制代码
mutex
叫互斥锁,等待锁的线程会处于休眠状态。
-(void)dealloc{
pthread_mutex_destroy(&_plock);
pthread_mutexattr_destroy(&t);
}
-(instancetype)init{
if (self =[super init]) {
//初始化锁的属性
// pthread_mutexattr_init(&t);
// pthread_mutexattr_settype(&t, PTHREAD_MUTEX_NORMAL);
// //初始化锁
// pthread_mutex_init(&_plock, &t);
pthread_mutex_t plock = PTHREAD_MUTEX_INITIALIZER;
self.plock = plock;
}
return self;
}
-(void)__saleTicket{
pthread_mutex_lock(&_plock);
[super __saleTicket];
pthread_mutex_unlock(&_plock);
}
- (void)__getMonery{
pthread_mutex_lock(&_plock);
[super __getMonery];
pthread_mutex_unlock(&_plock);
}
- (void)__saveMonery{
pthread_mutex_lock(&_plock);
[super __saveMonery];
pthread_mutex_unlock(&_plock);
}
//log
还剩 9 张票 - <NSThread: 0x6000014e3600>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x6000014c8d80>{number = 4, name = (null)}
还剩 7 张票 - <NSThread: 0x6000014c8f40>{number = 5, name = (null)}
还剩 4 张票 - <NSThread: 0x6000014c8f40>{number = 5, name = (null)}
还剩 3 张票 - <NSThread: 0x6000014c8f40>{number = 5, name = (null)}
还剩 5 张票 - <NSThread: 0x6000014c8d80>{number = 4, name = (null)}
还剩 6 张票 - <NSThread: 0x6000014e3600>{number = 3, name = (null)}
还剩 2 张票 - <NSThread: 0x6000014c8d80>{number = 4, name = (null)}
还剩 1 张票 - <NSThread: 0x6000014e3600>{number = 3, name = (null)}
复制代码
互斥锁有三个类型
/*
* Mutex type attributes
*/
普通锁
#define PTHREAD_MUTEX_NORMAL 0
//检查错误
#define PTHREAD_MUTEX_ERRORCHECK 1
//递归锁
#define PTHREAD_MUTEX_RECURSIVE 2
//普通锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
复制代码
当咱们这样子函数调用函数会出现死锁的问题,这是怎么出现的呢?第一把锁是锁住状态,而后进入第二个函数,锁在锁住状态,在等待,可是这把锁须要向后执行才会解锁,到时无限期的等待。
- (void)otherTest{
pthread_mutex_lock(&_plock);
NSLog(@"%s",__func__);
[self otherTest2];
pthread_mutex_unlock(&_plock);
}
- (void)otherTest2{
pthread_mutex_lock(&_plock);
NSLog(@"%s",__func__);
pthread_mutex_unlock(&_plock);
}
//log
-[FYPthread_mutex2 otherTest]
复制代码
上面这个需求须要使用两把锁,或者使用递归锁来解决问题。
- (void)otherTest{
pthread_mutex_lock(&_plock);
NSLog(@"%s",__func__);
[self otherTest2];
pthread_mutex_unlock(&_plock);
}
- (void)otherTest2{
pthread_mutex_lock(&_plock2);
NSLog(@"%s",__func__);
pthread_mutex_unlock(&_plock2);
}
//log
-[FYPthread_mutex2 otherTest]
-[FYPthread_mutex2 otherTest2]
复制代码
从使用2把锁是能够解决这个问题的。 递归锁是什么锁呢?容许同一个线程对一把锁重复加锁。
NSLock
是对mutex
普通锁的封装
使用(LLDB) si
能够跟踪[myLock lock];
的内部函数最终是pthread_mutex_lock
Foundation`-[NSLock lock]:
0x1090dfb5a <+0>: pushq %rbp
0x1090dfb5b <+1>: movq %rsp, %rbp
0x1090dfb5e <+4>: callq 0x1092ca3fe ; symbol stub for: object_getIndexedIvars
0x1090dfb63 <+9>: movq %rax, %rdi
0x1090dfb66 <+12>: popq %rbp
-> 0x1090dfb67 <+13>: jmp 0x1092ca596 ;
// symbol stub for: pthread_mutex_lock
复制代码
NSLock API
大全
//协议NSLocking
@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
复制代码
用法也很简单
@interface FYNSLock(){
NSLock *_lock;
}
@end
@implementation FYNSLock
- (instancetype)init{
if (self = [super init]) {
//封装了mutex的普通锁
_lock=[[NSLock alloc]init];
}
return self;
}
- (void)__saveMonery{
[_lock lock];
[super __saveMonery];
[_lock unlock];
}
- (void)__saleTicket{
[_lock lock];
[super __saleTicket];
[_lock unlock];
}
- (void)__getMonery{
[_lock lock];
[super __getMonery];
[_lock unlock];
}
@end
//log
还剩 9 张票 - <NSThread: 0x600003d4dc40>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x600003d4dc40>{number = 3, name = (null)}
还剩 7 张票 - <NSThread: 0x600003d4dc40>{number = 3, name = (null)}
还剩 6 张票 - <NSThread: 0x600003d7bfc0>{number = 4, name = (null)}
还剩 5 张票 - <NSThread: 0x600003d7bfc0>{number = 4, name = (null)}
还剩 4 张票 - <NSThread: 0x600003d7bfc0>{number = 4, name = (null)}
还剩 3 张票 - <NSThread: 0x600003d66c00>{number = 5, name = (null)}
还剩 2 张票 - <NSThread: 0x600003d66c00>{number = 5, name = (null)}
还剩 1 张票 - <NSThread: 0x600003d66c00>{number = 5, name = (null)}
复制代码
NSRecursiveLock
也是对mutex递归锁
的封装,API
跟NSLock
基本一致
- (BOOL)tryLock;//尝试加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;//日期前加锁
复制代码
递归锁能够对相同的线程进行反复加锁
@implementation FYRecursiveLockDemo
- (instancetype)init{
if (self = [super init]) {
//封装了mutex的递归锁
_lock=[[NSRecursiveLock alloc]init];
}
return self;
}
- (void)otherTest{
static int count = 10;
[_lock lock];
while (count > 0) {
count -= 1;
printf("循环% 2d次 - %s \n",count,[NSThread currentThread].description.UTF8String);
[self otherTest];
}
[_lock unlock];
}
@end
//log
循环 9次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 8次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 7次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 6次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 5次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 4次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 3次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 2次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 1次 - <NSThread: 0x60000274e900>{number = 1, name = main}
循环 0次 - <NSThread: 0x60000274e900>{number = 1, name = main}
复制代码
- (void)wait;//等待
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;//唤醒一个线程
- (void)broadcast;//唤醒多个线程
复制代码
NSCondition
是对mutex
和cond
的封装
- (instancetype)init{
if (self = [super init]) {
//遵照的 lock协议 的 条件🔐
_lock=[[NSCondition alloc]init];
self.array =[NSMutableArray array];
}
return self;
}
- (void)otherTest{
[[[NSThread alloc]initWithTarget:self selector:@selector(__remove) object:nil] start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__add) object:nil] start];
}
- (void)__add{
[_lock lock];
[self.array addObject:@"Test"];
NSLog(@"添加成功");
sleep(1);
[_lock signal];//唤醒一个线程
[_lock unlock];
}
- (void)__remove{
[_lock lock];
if (self.array.count == 0) {
[_lock wait];
}
[self.array removeLastObject];
NSLog(@"删除成功");
[_lock unlock];
}
@end
//Log
2019-07-29 10:06:48.904648+0800 day16--线程安全[43603:4402260] 添加成功
2019-07-29 10:06:49.907641+0800 day16--线程安全[43603:4402259] 删除成功
复制代码
能够看到时间上差了1秒,正好是咱们设定的sleep(1);
。优势是可让线程之间造成依赖,缺点是没有明确的条件。
NSConditionLock
是能够实现多个子线程进行线程间的依赖,A依赖于B执行完成,B依赖于C执行完毕则可使用NSConditionLock
来解决问题。 首先看下API
@property (readonly) NSInteger condition;//条件值
- (void)lockWhenCondition:(NSInteger)condition;//当con为condition进行锁住
//尝试加锁
- (BOOL)tryLock;
//当con为condition进行尝试锁住
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//当con为condition进行解锁
- (void)unlockWithCondition:(NSInteger)condition;
//NSDate 小余 limit进行 加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
//条件为condition 在limit以前进行加锁
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
复制代码
条件锁的使用,在lockWhenCondition:(NSInteger)condition
的条件到达的时候才能进行正常的加锁和unlockWithCondition:(NSInteger)condition
解锁,不然会阻塞线程。
- (void)otherTest{
[[[NSThread alloc]initWithTarget:self selector:@selector(__test2) object:nil] start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__test1) object:nil] start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__test3) object:nil] start];
}
- (void)__test1{
[_lock lockWhenCondition:1];
NSLog(@"%s",__func__);
[_lock unlockWithCondition:2];//解锁 并赋值2
}
- (void)__test2{
[_lock lockWhenCondition:2];
NSLog(@"%s",__func__);
[_lock unlockWithCondition:3];//解锁 并赋值3
}
- (void)__test3{
[_lock lockWhenCondition:3];
NSLog(@"%s",__func__);
[_lock unlockWithCondition:4];//解锁 并赋值4
}
@end
//log
-[FYCondLockDemo2 __test1]
-[FYCondLockDemo2 __test2]
-[FYCondLockDemo2 __test3]
复制代码
当con = 1
进行test1
加锁和执行任务A
,任务A
执行完毕,进行解锁,并把值2赋值给lock
,这是当con = 2
的锁开始加锁,进入任务B
,开始执行任务B
,当任务B
执行完毕,进行解锁并赋值为3,而后con=3
的锁进行加锁,解锁并赋值4来进行线程之间的依赖。
其实直接使用GCD的串行队列,也是能够实现线程同步的。串行队列其实就是线程的任务在队列中按照顺序执行,达到了锁的目的。
@interface FYSerialQueueDemo(){
dispatch_queue_t _queue;
}@end
@implementation FYSerialQueueDemo
- (instancetype)init{
if (self =[super init]) {
_queue = dispatch_queue_create("fyserial.queue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)__saleTicket{
dispatch_sync(_queue, ^{
[super __saleTicket];
});
}
- (void)__getMonery{
dispatch_sync(_queue, ^{
[super __getMonery];
});
}
- (void)__saveMonery{
dispatch_sync(_queue, ^{
[super __saveMonery];
});
}
@end
//log
还剩 9 张票 - <NSThread: 0x600001211b40>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x600001243700>{number = 4, name = (null)}
还剩 7 张票 - <NSThread: 0x60000121dd80>{number = 5, name = (null)}
还剩 6 张票 - <NSThread: 0x600001211b40>{number = 3, name = (null)}
还剩 5 张票 - <NSThread: 0x600001243700>{number = 4, name = (null)}
还剩 4 张票 - <NSThread: 0x60000121dd80>{number = 5, name = (null)}
还剩 3 张票 - <NSThread: 0x600001211b40>{number = 3, name = (null)}
还剩 2 张票 - <NSThread: 0x600001243700>{number = 4, name = (null)}
还剩 1 张票 - <NSThread: 0x60000121dd80>{number = 5, name = (null)}
复制代码
当咱们有大量任务须要并发执行,并且同时最大并发量为5个线程,这样子又该如何控制呢?dispatch_semaphore
信号量正好能够知足咱们的需求。 dispatch_semaphore
能够控制并发线程的数量,当设置为1时,能够做为同步锁来用,设置多个的时候,就是异步并发队列。
//初始化信号量 值为2,就是最多容许同时2个线程执行
_semaphore = dispatch_semaphore_create(2);
//生成多个线程进行并发访问test
- (void)otherTest{
for (int i = 0; i < 10; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil]start];
}
}
- (void)test{
//若是信号量>0 ,让信号量-1,继续向下执行。
//若是信号量 <= 0;就会等待,等待时间是 DISPATCH_TIME_FOREVER
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
sleep(2);//睡眠时间2s
NSLog(@"%@",[NSThread currentThread]);
//释放一个信号量
dispatch_semaphore_signal(_semaphore);
}
//log
2019-07-29 11:17:53.233318+0800 day16--线程安全[47907:4529610] <NSThread: 0x600002c45240>{number = 4, name = (null)}
2019-07-29 11:17:53.233329+0800 day16--线程安全[47907:4529609] <NSThread: 0x600002c45200>{number = 3, name = (null)}
2019-07-29 11:17:55.233879+0800 day16--线程安全[47907:4529616] <NSThread: 0x600002c45540>{number = 10, name = (null)}
2019-07-29 11:17:55.233879+0800 day16--线程安全[47907:4529612] <NSThread: 0x600002c45440>{number = 6, name = (null)}
2019-07-29 11:17:57.238860+0800 day16--线程安全[47907:4529613] <NSThread: 0x600002c45480>{number = 7, name = (null)}
2019-07-29 11:17:57.238867+0800 day16--线程安全[47907:4529614] <NSThread: 0x600002c454c0>{number = 8, name = (null)}
2019-07-29 11:17:59.241352+0800 day16--线程安全[47907:4529615] <NSThread: 0x600002c45500>{number = 9, name = (null)}
2019-07-29 11:17:59.241324+0800 day16--线程安全[47907:4529611] <NSThread: 0x600002c45400>{number = 5, name = (null)}
2019-07-29 11:18:01.245790+0800 day16--线程安全[47907:4529618] <NSThread: 0x600002c455c0>{number = 12, name = (null)}
2019-07-29 11:18:01.245790+0800 day16--线程安全[47907:4529617] <NSThread: 0x600002c45580>{number = 11, name = (null)}
复制代码
一次最多2个线程同时执行任务,暂停时间是2s。 使用信号量实现线程最大并发锁, 同时只有2个线程执行的。
- (instancetype)init{
if (self =[super init]) {
_semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)__saleTicket{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
[super __saleTicket];
dispatch_semaphore_signal(_semaphore);
}
//log
还剩 9 张票 - <NSThread: 0x6000022e0c00>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x6000022e0dc0>{number = 4, name = (null)}
还剩 7 张票 - <NSThread: 0x6000022ce880>{number = 5, name = (null)}
还剩 6 张票 - <NSThread: 0x6000022e0c00>{number = 3, name = (null)}
还剩 5 张票 - <NSThread: 0x6000022e0dc0>{number = 4, name = (null)}
还剩 4 张票 - <NSThread: 0x6000022ce880>{number = 5, name = (null)}
还剩 3 张票 - <NSThread: 0x6000022e0c00>{number = 3, name = (null)}
还剩 2 张票 - <NSThread: 0x6000022e0dc0>{number = 4, name = (null)}
还剩 1 张票 - <NSThread: 0x6000022ce880>{number = 5, name = (null)}
复制代码
@synchronized(id obj){}
锁的是对象obj
,使用该锁的时候,底层是对象计算出来的值做为key
,生成一把锁,不一样的资源的读写可使用不一样obj
做为锁对象。
- (void)__saleTicket{
@synchronized (self) {
[super __saleTicket];
}
}
//log
还剩 9 张票 - <NSThread: 0x60000057d5c0>{number = 3, name = (null)}
还剩 8 张票 - <NSThread: 0x60000056f340>{number = 4, name = (null)}
还剩 7 张票 - <NSThread: 0x60000057d500>{number = 5, name = (null)}
还剩 6 张票 - <NSThread: 0x60000057d5c0>{number = 3, name = (null)}
还剩 5 张票 - <NSThread: 0x60000056f340>{number = 4, name = (null)}
还剩 4 张票 - <NSThread: 0x60000057d500>{number = 5, name = (null)}
还剩 3 张票 - <NSThread: 0x60000057d5c0>{number = 3, name = (null)}
还剩 2 张票 - <NSThread: 0x60000056f340>{number = 4, name = (null)}
还剩 1 张票 - <NSThread: 0x60000057d500>{number = 5, name = (null)}
复制代码
给属性添加atmoic
修饰,能够保证属性的setter
和getter
都是原子性操做,也就保证了setter
和getter
的内部是线程同步的。 原子操做是最终调用了static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) objc-accessors.mm 48行
,咱们进入到函数内部
//设置属性原子操做
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
//非原子操做设置属性
void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{//偏移量等于0则是class指针
if (offset == 0) {
object_setClass(self, newValue);
return;
}
//其余的value
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
//若是是copy 用copyWithZone:
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
//mutableCopy则调用mutableCopyWithZone:
newValue = [newValue mutableCopyWithZone:nil];
} else {
//若是赋值和原来的相等 则不操做
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {//非原子操做 直接赋值
oldValue = *slot;
*slot = newValue;
} else {//原子操做 加锁
//锁和属性是一一对应的->自旋锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;//赋值
slotlock.unlock();//解锁
}
objc_release(oldValue);
}
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;//非原子操做 直接返回值
// Atomic retain release world
//原子操做 加锁->自旋锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();//加锁
id value = objc_retain(*slot);
slotlock.unlock();//解锁
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
//以属性的地址为参数计算出key ,锁为value
StripedMap<spinlock_t> PropertyLocks;
复制代码
从源码了解到设置属性读取是self
+属性的偏移量,当copy
或mutableCopy
会调用到[newValue copyWithZone:nil]
或[newValue mutableCopyWithZone:nil]
,若是新旧值相等则不进行操做,非原子操做直接赋值,原子操做则获取spinlock_t& slotlock = PropertyLocks[slot]
进行加锁、赋值、解锁操做。并且PropertyLocks
是一个类,类有一个数组属性,使用*p
计算出来的值做为key
。
咱们提取出来关键代码
//原子操做 加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;//赋值
slotlock.unlock();//解锁
复制代码
使用自旋锁对赋值操做进行加锁,保证了setter()
方法的安全性
//原子操做 加锁 ->自旋锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();//加锁
id value = objc_retain(*slot);
slotlock.unlock();//解锁
复制代码
取值以前进行加锁,取值以后进行解锁,保证了getter()
方法的安全。
由上面得知atmoic
仅仅是对方法setter()
和getter()
安全,对成员变量不保证安全,对于属性的读写通常使用nonatomic
,性能好,atomic
读取频率高的时候会致使线程都在排队,浪费CPU时间。
大概使用者几种锁分别对卖票功能进行了性能测试, 性能分别1万次、100万次、1000万次锁花费的时间对比,单位是秒。(仅供参考,不一样环境时间略有差别)
锁类型 | 1万次 | 100万次 | 1000万次 |
---|---|---|---|
pthread_mutex_t | 0.000309 | 0.027238 | 0.284714 |
os_unfair_lock | 0.000274 | 0.028266 | 0.285685 |
OSSpinLock | 0.030688 | 0.410067 | 0.437702 |
NSCondition | 0.005067 | 0.323492 | 1.078636 |
NSLock | 0.038692 | 0.151601 | 1.322062 |
NSRecursiveLock | 0.007973 | 0.151601 | 1.673409 |
@synchronized | 0.008953 | 0.640234 | 2.790291 |
NSConditionLock | 0.229148 | 5.325272 | 10.681123 |
semaphore | 0.094267 | 0.415351 | 24.699100 |
SerialQueue | 0.213386 | 9.058581 | 50.820202 |
平时咱们简单使用的话没有很大的区别,仍是推荐使用NSLock
和信号量,最简单的是@synchronized
,不用声明和初始化,直接拿来就用。
自旋锁和互斥锁各有优劣,代码执行频率高,CPU充足,可使用互斥锁,频率低,代码复杂则须要互斥锁。
一个简单的读写锁,读写互斥便可,咱们使用信号量,值设定为1.同时只能一个线程来操做文件,读写互斥。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.semaphore = dispatch_semaphore_create(1);
for (NSInteger i = 0; i < 10; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
}
}
- (void)read{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%s",__func__);
dispatch_semaphore_signal(self.semaphore);
}
- (void)write{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"%s",__func__);
dispatch_semaphore_signal(self.semaphore);
}
复制代码
当读写都是一个线程来操做,会下降性能,当多个线程在读资源的时候,其实不须要同步操做的,有读没写,理论上说不用限制异步数量,写入的时候不能读,才是真正限制线程性能的地方,读写锁具有如下特色
典型的多读单写
,常常用于文件等数据的读写操做,咱们实现2种
这是有c语言封装的读写锁
//初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
const pthread_rwlockattr_t * _Nullable __restrict)
//读上锁
pthread_rwlock_rdlock(pthread_rwlock_t *)
//尝试加锁读
pthread_rwlock_tryrdlock(pthread_rwlock_t *)
//尝试加锁写
int pthread_rwlock_trywrlock(pthread_rwlock_t *)
//写入加锁
pthread_rwlock_wrlock(pthread_rwlock_t *)
//解锁
pthread_rwlock_unlock(pthread_rwlock_t *)
//销毁锁属性
pthread_rwlockattr_destroy(pthread_rwlockattr_t *)
//销毁锁
pthread_rwlock_destroy(pthread_rwlock_t * )
复制代码
pthread_rwlock_t
使用很简单,只须要在读以前使用pthread_rwlock_rdlock
,读完解锁pthread_rwlock_unlock
,写入前须要加锁pthread_rwlock_wrlock
,写入完成以后解锁pthread_rwlock_unlock
,任务都执行完了能够选择销毁pthread_rwlock_destroy
或者等待下次使用。
@property (nonatomic,assign) pthread_rwlock_t rwlock;
- (void)dealloc{
pthread_rwlock_destroy(&_rwlock);//销毁锁
}
//初始化读写锁
pthread_rwlock_init(&_rwlock, NULL);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i ++) {
dispatch_async(queue, ^{
[[[NSThread alloc]initWithTarget:self selector:@selector(readPthreadRWLock) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(writePthreadRWLock) object:nil]start];
});
}
- (void)readPthreadRWLock{
pthread_rwlock_rdlock(&_rwlock);
NSLog(@"读文件");
sleep(1);
pthread_rwlock_unlock(&_rwlock);
}
- (void)writePthreadRWLock{
pthread_rwlock_wrlock(&_rwlock);
NSLog(@" 写入文件");
sleep(1);
pthread_rwlock_unlock(&_rwlock);
}
//log
2019-07-30 10:47:16 读文件
2019-07-30 10:47:16 读文件
2019-07-30 10:47:17 写入文件
2019-07-30 10:47:18 写入文件
2019-07-30 10:47:19 读文件
2019-07-30 10:47:19 读文件
2019-07-30 10:47:19 读文件
2019-07-30 10:47:20 写入文件
2019-07-30 10:47:21 写入文件
2019-07-30 10:47:22 写入文件
复制代码
读文件会出现同一秒读屡次,写文件同一秒只有一个。
栅栏你们都见过,为了分开一个地区而使用的,线程的栅栏函数是分开任务的执行顺序
操做 | 任务 | 任务 | 任务 |
---|---|---|---|
读 | A | B | |
读 | A | B | |
写 | C | ||
写 | C | ||
读 | A | ||
读 | A | B |
这个函数传入的并发队列必须是经过dispatch_queue_create
建立,若是传入的是一个串行的或者全局并发队列,这个函数便等同于dispatch_async
的效果。
//初始化 异步队列
self.rwqueue = dispatch_queue_create("rw.thread", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i ++) {
dispatch_async(queue, ^{
[self readBarryier];
[self readBarryier];
[self readBarryier];
[self writeBarrier];
});
}
- (void)readBarryier{
//添加任务到rwqueue
dispatch_async(self.rwqueue, ^{
NSLog(@"读文件 %@",[NSThread currentThread]);
sleep(1);
});
}
- (void)writeBarrier{
//barrier_async添加任务到self.rwqueue中
dispatch_barrier_async(self.rwqueue, ^{
NSLog(@"写入文件 %@",[NSThread currentThread]);
sleep(1);
});
}
//log
2019-07-30 11:16:53 读文件 <NSThread: 0x600001ae0740>{number = 9, name = (null)}
2019-07-30 11:16:53 读文件 <NSThread: 0x600001ae8500>{number = 10, name = (null)}
2019-07-30 11:16:53 读文件 <NSThread: 0x600001ae8040>{number = 8, name = (null)}
2019-07-30 11:16:53 读文件 <NSThread: 0x600001ac3a80>{number = 11, name = (null)}
2019-07-30 11:16:54 写入文件<NSThread: 0x600001ac3a80>{number = 11, name = (null)}
2019-07-30 11:16:55 写入文件<NSThread: 0x600001ac3a80>{number = 11, name = (null)}
2019-07-30 11:16:56 写入文件<NSThread: 0x600001ac3a80>{number = 11, name = (null)}
复制代码
读文件会出现同一秒读多个,写文件同一秒只有一个。
读写任务都添加到异步队列rwqueue
中,使用栅栏函数dispatch_barrier_async
拦截一下,实现读写互斥,读能够异步无限读,写只能一个同步写的功能。
atomic
原子操做只限制setter
和getter
方法,不限制成员变量pthread_rwlock_t
和dispatch_barrier_async
最怕一辈子碌碌无为,还安慰本身平凡难得。
广告时间