iOS底层-多线程(一)
简介
- iOS中的常见多线程方案
- GCD的常用函数
- GCD中有2个用来执行任务的函数
-
用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block); queue:队列 block:任务
-
用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
-
- GCD源码:https://github.com/apple/swift-corelibs-libdispatch
- GCD中有2个用来执行任务的函数
- GCD的队列
- GCD的队列可以分为2大类型
- 并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效
- 串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
- 并发队列(Concurrent Dispatch Queue)
- GCD的队列可以分为2大类型
- 容易混淆的术语
- 有4个术语比较容易混淆:同步、异步、并发、串行
- 同步和异步主要影响:能不能开启新的线程
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 并发和串行主要影响:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
- 同步和异步主要影响:能不能开启新的线程
- 有4个术语比较容易混淆:同步、异步、并发、串行
-
各种队列的执行效果
- 注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)!!!
- 使用使用sync函数
- 当前队列
- 当前队列是串行队列
-
所谓的死锁就是卡主当前的线程崩溃
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"==任务1=="); //1. 拿到当前的队列-主队列 dispatch_queue_t queue = dispatch_get_main_queue(); //2. 将block这个任务放到主队列中 //队列特点:FIFO,先进先出. //dispatch_sync:立马在当前线程执行block,执行完才能往下走 dispatch_sync(queue, ^{ NSLog(@"==任务2=="); }); NSLog(@"==任务3=="); }
- 分析为何死锁:
- 队列是存放任务的,而且任务是按一定顺序的
- 当前主队列的任务:
1.viewDidLoad 2. 任务2
- 当前主队列的任务:
- 线程是执行任务的,按照队列中的任务顺序执行任务
- 正常情况下:
- 执行viewDidLoad任务
- 任务1
- dispatch_sync函数
- 任务3
- 执行“任务2”block
- 执行viewDidLoad任务
- 但是在执行viewDidLoad内部的dispatch_sync函数时,该函数的目的是将“任务2”立刻执行,执行完才能继续执行任务3
- 由于任务2是排在viewDidLoad后面的,因此dispatch_sync函数肯定取”任务2”失败,导致函数不能返回,而“任务3”又是在dispatch_sync函数执行完之后才能执行,所以造成了卡死
- 正常情况下:
- 队列是存放任务的,而且任务是按一定顺序的
- 分析为何死锁:
-
如果使用dispatch_async就不会卡死
/** dispatch_async函数不要求立刻在当前线程执行block里面的任务,因此,不会卡线程 */ dispatch_async(queue, ^{ NSLog(@"==任务2=="); });
-
打印:
==任务1== ==任务3== ==任务2==
-
- 分析死锁的关键:线程执行的任务顺序 是否跟 队列中存放的任务顺序一致,一致则没问题,不一致则卡住
- 注意:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)!!!
知识点
-
下面代码的打印结果是什么?
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ //在子线程中调用 dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"1"); //这个没有问题: 1,2,3 // [self performSelector:@selector(test) withObject:self]; //打印: 1,3 [self performSelector:@selector(test) withObject:self afterDelay:1]; NSLog(@"3"); }); //在主线程中调用 //1,3,2 // NSLog(@"1"); // [self performSelector:@selector(test) withObject:self afterDelay:1]; // NSLog(@"3"); } -(void)test{ NSLog(@"2"); }
- 打印结果为:1,3
performSelector:withObject:afterDelay:
底层分析- 右击进入该方法,发现在NSRunloop.h文件中定义的,因此一定跟runloop有关
- 猜测底层:
- 底层先定义了一个NStimer定时器
- 然后将定时器添加到runloop中
- 在主线程中自动开启了主线程runloop,所以能够正常执行
- 但是在新开的子线程中,并没有手动(子线程需要手动开启runloop)开启runloop,那么子线程一但代码执行完成就死了,等延迟时间到以后,子线程已死,当然无法执行
- 所以无法执行2
-
这么就能够调用正常了
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"1"); //打印: 1,3,2 [self performSelector:@selector(test) withObject:self afterDelay:1]; NSLog(@"3"); //子线程需要手动开启runloop [[NSRunLoop currentRunLoop] run]; });
-
下面代码的打印结果是什么?
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSThread *thread = [[NSThread alloc] initWithBlock:^{ NSLog(@"1"); }]; [thread start]; [self performSelector:@selector(test) onThread:thread withObject:self waitUntilDone:YES]; } -(void)test{ NSLog(@"2"); }
- 打印: 1,然后崩溃
- 因为start之后,会在子线程执行block,执行完成以后,子线程thread已经销毁,因此再去执行test会崩溃
-
可以在block内部添加runloop,让线程thread不死,这么就可以执行test了
NSThread *thread = [[NSThread alloc] initWithBlock:^{ NSLog(@"1"); [[NSRunLoop currentRunLoop] addPort:NSPort.port forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }];
GNUstep
- 苹果的Foundation框架是不开源的,那么如果我们想看苹果的源码该如何办? 比如NSArray、NSRunloop等
- 方法一:通过打断点->debug->debug workFlow->Disassembly ,转化成汇编来分析
- 方法二:通过GNUstep查看
- GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
- 源码地址:http://www.gnustep.org/resources/downloads.php
- 虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
- 下载foundation框架:
GNUstep Base
,查看performSelector:withObject:afterDelay:
方法实现:- 的确是定义了一个定时器,然后将定时器添加到当前线程的runloop中,但是并没有开启这个线程runloop。
- 汇编调试小知识,重要:!!!!!!
- 断点进入汇编时,如果想要汇编一句一句执行,而且遇到函数调用
call ...
进入函数,那就用si
指令; 如果不进入函数,就用ni
指令。
- 断点进入汇编时,如果想要汇编一句一句执行,而且遇到函数调用
队列组的使用
- 背景简介
- 平时在进行多线程处理任务时,有时候希望多个任务之间存在着一种联系,希望在所有的任务执行完后做一些总结性处理。
- 那么就可以将多个任务放在一个任务组中进行统一管理。dispatch提供了相应的API供我们完成这一需求。
-
dispatch_group_t相关API介绍
//1. 将block任务添加到queue队列,并被group组管理 dispatch_group_async(group, queue, block); //2. 声明dispatch_group_enter(group)下面的任务由group组管理,group组的任务数+1 dispatch_group_enter(group); //3. 相应的任务执行完成,group组的任务数-1 dispatch_group_leave(group); //4. 创建一个group组 dispatch_group_create(); //5. 等待tiemr秒的时间,不管队列中任务是否完成,都继续往下执行,如果该时间内任务完成了就返回0,否则是非零值 dispatch_group_wait(group1, DISPATCH_TIME_FOREVER); //6. 监听group组中任务的完成状态,当所有的任务都执行完成后,触发block块,执行总结性处理。 dispatch_group_notify(group1, queue1,block);
- 常见用法的区别
- 在使用group组处理任务时,常见的有两种组合。
-
第一种:
dispatch_group_async(group, queue, block); dispatch_group_notify(group1, queue1, block);
- 在这种组合下,根据任务是同步、异步又分为两种,这两种组合的执行代码与运行结果如下
-
同步任务:
dispatch_queue_t queue1 = dispatch_queue_create("dispatchGroupMethod1.queue1", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group1 = dispatch_group_create(); dispatch_group_async(group1, queue1, ^{ dispatch_sync(queue1, ^{ for (NSInteger i =0; i<3; i++) { sleep(1); NSLog(@"%@-同步任务执行-:%ld",@"任务1",(long)i); } }); }); dispatch_group_async(group1, queue1, ^{ dispatch_sync(queue1, ^{ for (NSInteger i =0; i<3; i++) { sleep(1); NSLog(@"%@-同步任务执行-:%ld",@"任务2",(long)i); } }); }); //等待上面的任务全部完成后,会往下继续执行 (会阻塞当前线程) // dispatch_group_wait(group1, DISPATCH_TIME_FOREVER); //等待上面的任务全部完成后,会收到通知执行block中的代码 (不会阻塞线程) dispatch_group_notify(group1, queue1, ^{ NSLog(@"Method1-全部任务执行完成"); });
-
同步任务运行结果:
任务1-同步任务执行-:0 任务1-同步任务执行-:0 任务1-同步任务执行-:1 任务1-同步任务执行-:1 任务1-同步任务执行-:2 任务1-同步任务执行-:2 Method1-全部任务执行完成
-
-
异步任务:
-
将上面的任务都改成:
dispatch_async(queue1, ^{ for (NSInteger i =0; i<3; i++) { sleep(1); NSLog(@"%@-异步任务执行-:%ld",@"任务1",(long)i); } });
-
执行结果:
Method1-全部任务执行完成 任务2-异步任务执行-:0 任务1-异步任务执行-:0 任务2-异步任务执行-:1 任务1-异步任务执行-:1 任务2-异步任务执行-:2 任务1-异步任务执行-:2
-
结论:dispatch_group_async(group, queue, block) 和 dispatch_group_notify(group1, queue1, block) 组合在执行同步任务时正常,在执行异步任务时不正常。
-
-
-
- 在这种组合下,根据任务是同步、异步又分为两种,这两种组合的执行代码与运行结果如下
-
第二种:
dispatch_group_enter(group); dispatch_group_leave(group); dispatch_group_notify(group1, queue1,block);
-
同步任务:
dispatch_queue_t queue2 = dispatch_queue_create("dispatchGroupMethod2.queue2", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group2 = dispatch_group_create(); dispatch_group_enter(group2); dispatch_sync(queue2, ^{ for (NSInteger i =0; i<3; i++) { sleep(1); NSLog(@"%@-同步任务执行-:%ld",@"任务1",(long)i); } dispatch_group_leave(group2); }); dispatch_group_enter(group2); dispatch_sync(queue2, ^{ for (NSInteger i =0; i<3; i++) { sleep(1); NSLog(@"%@-同步任务执行-:%ld",@"任务2",(long)i); } dispatch_group_leave(group2); }); //等待上面的任务全部完成后,会往下继续执行 (会阻塞当前线程) // dispatch_group_wait(group2, DISPATCH_TIME_FOREVER); //等待上面的任务全部完成后,会收到通知执行block中的代码 (不会阻塞线程) dispatch_group_notify(group2, queue2, ^{ NSLog(@"Method2-全部任务执行完成"); });
- 打印,没问题,同步任务正常
-
异步任务:
-
将上面的任务换成:
dispatch_async(queue2, ^{ for (NSInteger i =0; i<3; i++) { sleep(1); NSLog(@"%@-异步任务执行-:%ld",@"任务1",(long)i); } dispatch_group_leave(group2); });
- 打印正常
- 结论: dispatch_group_enter(group)、dispatch_group_leave(group) 和 dispatch_group_notify(group1, queue1,block) 组合在执行同步任务时正常,在执行异步任务时正常。
-
-
-
- 在使用group组处理任务时,常见的有两种组合。
多线程的安全隐患
- 资源共享
- 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
- 比如多个线程访问同一个对象、同一个变量、同一个文件
- 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
- 多线程安全隐患分析
-
多线程安全隐患的解决方案
- 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
- 常见的线程同步技术是:加锁
- 线程一访问数据开始,将数据锁住,不允许其他线程访问,直到当前线程访问完毕,锁打开,其他线程允许访问,而且同样上锁
-
代码举例:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [self saleTickets]; } -(void)saleTicket{ int oldTicketsCount = self.ticketsCount; sleep(.2); oldTicketsCount--; self.ticketsCount = oldTicketsCount; NSLog(@"还剩%d张票====%@",oldTicketsCount,[NSThread currentThread]); } -(void)saleTickets{ self.ticketsCount = 15; dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i =0; i<2; i++) { [self saleTicket]; } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i =0; i<2; i++) { [self saleTicket]; } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i =0; i<2; i++) { [self saleTicket]; } }); }
-
打印:
还剩12张票====<NSThread: 0x60000129db80>{number = 5, name = (null)} 还剩13张票====<NSThread: 0x60000129dc40>{number = 4, name = (null)} 还剩14张票====<NSThread: 0x6000012f4980>{number = 3, name = (null)} 还剩11张票====<NSThread: 0x60000129dc40>{number = 4, name = (null)} 还剩11张票====<NSThread: 0x6000012f4980>{number = 3, name = (null)} 还剩10张票====<NSThread: 0x60000129db80>{number = 5, name = (null)}
-
可以看到很显然是错误的
-
什么时候采用用到锁呢?(注意理解!!!)
- 多条线程访问一个资源的时候
- 这个资源可以理解为一个数组、字典、甚至是一个属性
- 如果是简单的读取,不影响,但是如果需要改变,那就必须加锁了
-
比如:
for (int i = 0;i<5;i++) { NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil]; [thread start]; } //n个线程调用同一个方法 - (void)test { int a = 10; int b = 5; //如果修改需要加锁 self.age = 10; //仅仅是访问,就没必要 NSLog(@"%d",self.age); }
iOS中的线程同步方案
-
主要有以下方案
OSSpinLock os_unfair_lock pthread_mutex dispatch_semaphore dispatch_queue(DISPATCH_QUEUE_SERIAL) NSLock NSRecursiveLock NSCondition NSConditionLock @synchronized
OSSpinLock
- OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
- 目前已经不再安全,可能会出现优先级反转问题
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
- 线程有优先级的,优先级高的线程,CPU执行的次数多
- 假设线程1的优先级比线程2高
- 当前是线程2访问资源,然后加锁
- 但此时线程1也来访问资源,发现资源已经被加锁,那么线程1就一直等待,由于线程1的优先级比较高,那么可能CPU就一直使用线程1,导致线程2不能正常的访问资源,然后解锁
- 从而造成线程1一直占用这CPU资源,线程2不能正常解锁
-
需要导入头文件#import <libkern/OSAtomic.h>
#import "ViewController.h" #import <libkern/OSAtomic.h> @interface ViewController () @property (nonatomic, assign) int ticketsCount; @property (nonatomic, assign) int money; @property (nonatomic, assign) OSSpinLock lock; @property (nonatomic, assign) OSSpinLock lock1; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.lock = OS_SPINLOCK_INIT; self.lock1 = OS_SPINLOCK_INIT; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // [self saleTicketTest]; [self moneyTest]; } /** 存取钱演示 */ -(void)moneyTest{ self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i =0; i<3; i++) { [self saveMoney]; } }); dispatch_async(queue, ^{ for (int i =0; i<3; i++) { [self drawMoney]; } }); } /** 存钱 */ -(void)saveMoney{ //加锁 OSSpinLockLock(&_lock1); int oldMoney = self.money; sleep(.2); oldMoney+= 50; self.money = oldMoney; NSLog(@"存50,还剩%d元====%@",oldMoney,[NSThread currentThread]); //解锁 OSSpinLockUnlock(&_lock1); } /** 取钱 */ -(void)drawMoney{ //加锁 OSSpinLockLock(&_lock1); int oldMoney = self.money; sleep(.2); oldMoney-= 20; self.money = oldMoney; NSLog(@"取20,还剩%d元====%@",oldMoney,[NSThread currentThread]); //解锁 OSSpinLockUnlock(&_lock1); } /** 卖一张票 */ -(void)saleTicket{ //注意整个过程要用同一把锁,不能每调用一次都重新创建一把锁 //初始化 // OSSpinLock lock = OS_SPINLOCK_INIT; //方法1: //尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true) // bool result = OSSpinLockTry(&_lock); // if (result){ // int oldTicketsCount = self.ticketsCount; // sleep(.2); // oldTicketsCount--; // self.ticketsCount = oldTicketsCount; // NSLog(@"还剩%d张票====%@",oldTicketsCount,[NSThread currentThread]); // //解锁 // OSSpinLockUnlock(&_lock); // }; //方法二: //加锁(先查看当前是否被加锁,没有才加锁。 如果每次都创建一个lock,那么每次锁都是新的,当然没有加,所以每次都会加) OSSpinLockLock(&_lock); int oldTicketsCount = self.ticketsCount; sleep(.2); oldTicketsCount--; self.ticketsCount = oldTicketsCount; NSLog(@"还剩%d张票====%@",oldTicketsCount,[NSThread currentThread]); //解锁 OSSpinLockUnlock(&_lock); } /** 卖多张票 */ -(void)saleTicketTest{ self.ticketsCount = 15; dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i =0; i<2; i++) { [self saleTicket]; } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i =0; i<2; i++) { [self saleTicket]; } }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ for (int i =0; i<2; i++) { [self saleTicket]; } }); } @end
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁