前言
明天要给师弟开分享会,分享GCD。 好方,只理解一些皮毛拿什么去装。准备的时候顺便把过程记录下来。
目录
- 概念
- 简单了解用法
- 开发中常用的做法
- GCD其他的一些API
- GCD会遇到的问题
和GCD有关的基本概念
术语 | 含义 |
---|---|
进程 | 打开一个App就是开启一个进程。 |
线程 | 独立执行的代码段,一个线程同时间只能执行一个任务,反之多线程并发就可以在同一时间执行多个任务。在iOS系统中,一个进程包含一个主线程,它的主要任务是处理UI。其他线程称为子线程。 |
同步 | A执行完再执行B。 |
异步 | A和B可以同时执行。 |
任务 | 可以理解为某一堆要执行的代码。分为同步执行任务和异步执行任务。用block定义。 |
同步执行任务 | 按进入顺序执行的任务 |
异步执行任务 | 不管进入顺序,可以一起执行 |
队列 | 存放任务的结构。分为串行队列和并行队列。遵循先进先出。 |
队列组 | 将多线程进行分组,最大的好处是可获知所有线程的完成情况。 |
串行队列 | 线程执行只能依次逐一先后有序的执行。 |
并行队列 | 指两个或多个事件在同一时刻发生。多核CUP同时开启多条线程供多个任务同时执行,互不干扰。 |
并发 | 指两个或多个事件在同一时间间隔内发生。可以在某条线程和其他线程之间反复多次进行上下文切换,看上去就好像一个CPU能够并且执行多个线程一样。其实是伪异步。 |
- 一个有助于判断执行完成时间的理论。 开线程需要消耗内存,所以要消耗时间。
回头看觉得有必要在这简单说明多线程 多核 并发并行的区别 和 子线程和主线程的联系。
最近玩了个游戏叫《Inside》,戴着头盔就能操纵机器人,感觉无论是玩法还是游戏剧情都超适合类比线程。 用这个举个例子。 假如你是国王,拿到了一张藏宝图,但这个宝藏要到每一个地点才能得知下一个地点的信息(电路中内存地址)。于是你就操纵机器人A去找,找到后带回来。机器人A的路线就是一条线程。 当机器人A还在路程上,你又得到一张藏宝图。你这时候派机器人B去找,找到带回来。这时候机器人B的路线就是另一条线程。 以上就是多线程。 这时候,只要你周期足够短,轮流戴头盔a和头盔b,,看上去就像你同时在操纵机器人A和机器人B。这就叫做并发!装出来的。 某一天,你的头快摇傻了。于是乎你长出了第二个头。(对应着双核CPU),这时候就是名副其实地同时操纵。这就叫并行,必须要多头怪才拥有这技能。 但如果又操纵第三个机器人,这时候只能再来回戴了,又要并发了。 A找到并回到了城堡把结果带回给你,你才发现你也是个机器人(主线程)。其他机器人带回宝藏后就可以拜拜了,但就算还有没有宝藏在路上,你都不能拜拜,必须保持呼吸(runloop)。 这就是子线程和主线程的联系。 子线程的任务全部完成后,最终会回到主线程。主线程中运行着runloop
简单了解用法
就是把任务加到队列中 队列可以自己新建。 系统也有 全局并发队列和主队列。
#pragma mark - 创建队列// 创建队列// 第一个参数 队列名称// 第二个参数的作用:串行(DISPATCH_QUEUE_SERIAL)、并行(DISPATCH_QUEUE_CONCURRENT)。 dispatch_queue_t queue = dispatch_queue_create("net.Hsusue.testQueue", DISPATCH_QUEUE_CONCURRENT);* 常用的系统并发队列——全局并发队列//程序默认的队列级别,一般不要修改,DISPATCH_QUEUE_PRIORITY_DEFAULT == 0dispatch_queue_t globalQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//HIGHdispatch_queue_t globalQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);//LOWdispatch_queue_t globalQueue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);//BACKGROUNDdispatch_queue_t globalQueue4 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);// 获取主队列有特别函数(是个串行队列)// dispatch_queue_t queue = dispatch_get_main_queue();#pragma mark - 创建任务加到队列中// 同步执行任务创建方法 dispatch_sync(queue, ^{ // 这里放同步执行任务代码 });// 异步执行任务创建方法 dispatch_async(queue, ^{ // 这里放异步执行任务代码 });复制代码
个人认为易迷惑的点
- 太多的组合方式 有两种任务执行方式,两种队列+特殊的主队列,就可以组成六种组合。 有两张图总结得特别好,记住这两张图,分析的时候用得到。 然后为了更好理解,自己也花了点时间弄了动图。
还是不能忘了《Inside》的例子。
-
两种待办任务表(对应队列) 一种是多个机器人对多个宝藏,先入先出发。(对应并行队列) 另一种是一个机器人对有顺序找的多个宝藏。(对应串行队列) 特殊的 强行自己去做的任务表。 (对应主队列)
-
你有两类事情(对应任务) 一类是吃喝拉撒,一有需要就自己马上去做,总不能懒到让机器人帮忙吧。(对应着同步执行任务) 另一类是寻宝,要机器人去做,出发前要点时间给机器人充电。(对应着异步执行任务)
代码中, 输出@"1"对应着吃喝拉撒
- 异步 + 并行队列 (多个机器人找多个宝藏)
- (void)viewDidLoad { [super viewDidLoad]; [self asyncConcurrent]; NSLog(@"1");}//异步执行 + 并行队列- (void)asyncConcurrent{ //创建一个并行队列 dispatch_queue_t queue = dispatch_queue_create("标识符", DISPATCH_QUEUE_CONCURRENT); NSLog(@"---start---"); //使用异步函数封装三个任务 dispatch_async(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务B---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务C---%@", [NSThread currentThread]); }); NSLog(@"---end---");}复制代码
- 异步 + 串行队列 (一个机器人找有序宝藏)
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent]; [self asyncSerial]; NSLog(@"1---%@", [NSThread currentThread]);}//异步 + 串行队列- (void)asyncSerial{ //创建一个串行队列 dispatch_queue_t queue = dispatch_queue_create("标识符", DISPATCH_QUEUE_SERIAL); NSLog(@"---start---"); //使用异步函数封装三个任务 dispatch_async(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务B---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务C---%@", [NSThread currentThread]); }); NSLog(@"---end---");}复制代码
- 同步 + 并行队列 (自己吃喝拉撒 放到 多个机器人对多个宝藏,准备好后机器人一起出发)
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent];// [self asyncSerial]; [self syncConcurrent]; NSLog(@"1---%@", [NSThread currentThread]);}//同步 + 并行队列- (void)syncConcurrent{ //创建一个并行队列 dispatch_queue_t queue = dispatch_queue_create("标识符", DISPATCH_QUEUE_CONCURRENT); NSLog(@"---start---"); //使用同步函数封装三个任务 dispatch_sync(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务B---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务C---%@", [NSThread currentThread]); }); NSLog(@"---end---");}复制代码
- 同步+ 串行队列 (自己吃喝拉撒 放到 一个机器人对有顺序找的多个宝藏)
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent];// [self asyncSerial];// [self syncConcurrent] [self syncSerial]; NSLog(@"1---%@", [NSThread currentThread]);}//同步 + 串行队列- (void)syncSerial{ //创建一个串行队列 dispatch_queue_t queue = dispatch_queue_create("标识符", DISPATCH_QUEUE_SERIAL); NSLog(@"---start---"); //使用异步函数封装三个任务 dispatch_sync(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务B---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务C---%@", [NSThread currentThread]); }); NSLog(@"---end---");}复制代码
一要吃喝拉撒就自己马上去做。所以不等@“end”输出就先做完了。最后再@“1”。有着必然先后顺序。
- 异步 + 主队列 (让机器人充电准备寻宝 放到 强行自身去做的任务表 )
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent];// [self asyncSerial];// [self syncConcurrent]// [self syncSerial]; [self asyncMain]; NSLog(@"1---%@", [NSThread currentThread]);}//异步 + 主队列- (void)asyncMain{ //获取主队列 dispatch_queue_t queue = dispatch_get_main_queue(); NSLog(@"---start---"); //使用异步函数封装三个任务 dispatch_async(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务B---%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务C---%@", [NSThread currentThread]); }); NSLog(@"---end---");}复制代码
和异步 + 串行队列区别就是不开启新线程。
- 同步+主队列(死锁)(吃喝拉撒 + 强行自身去做)
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent];// [self asyncSerial];// [self syncConcurrent]// [self syncSerial];// [self asyncMain]; [self syncMain]; NSLog(@"1---%@", [NSThread currentThread]);}//同步+主队列(死锁)- (void)syncMain{ //获取主队列 dispatch_queue_t queue = dispatch_get_main_queue(); NSLog(@"---start---"); //使用同步函数封装三个任务 dispatch_sync(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务B---%@", [NSThread currentThread]); }); dispatch_sync(queue, ^{ NSLog(@"任务C---%@", [NSThread currentThread]); }); NSLog(@"---end---");}复制代码
计算机指令包括操作码和地址码。
每个函数进入都会记住进入的地址码,return时就会回去。
上面主队列在主队列中加了任务。 实质在同一个同步串行队列中,再使用该串行队列同步的执行任务。
[self syncMain]这是主队列做(出)的事(同步且未做完)。根据先进先出,主队列头是syncMain。然后假设这里的内存地址是1。
dispatch_sync(queue, ^{ NSLog(@"任务A---%@", [NSThread currentThread]); });// 假设运行时此处内存地址为1复制代码
添加了一个block到主队列尾部,要等主队列头synMain执行完才能执行。 本来应该执行追加任务B,但是电路上的地址并没有回来,因为dispatch_sync
要执行完block才reutrn。 因为被代码被黑盒子包起来了,大胆猜测一下。 假设内存地址为2
// 调用时记住进入地址为1dispatch_sync { // block执行完才return // 运行时此处内存地址为2 if( block() ) { // block执行完 return;//返回到进入地址 }}复制代码
于是代码可以看成 卡在了该函数内部,内存地址为2处。 没有回到1处,自然就不会追加任务B。
开发中常用的做法
上面说了很多种方法,禁止死锁情况开发中是很容易记住的。 但其他组合,即使想的时候能想懂,但也还是很混乱。 根据我个人经验,日常开发中先从宏观上想是否需要耗时(耗时放到子线程),是否有序。 通常是需要和主线程同时执行(开新线程,即异步执行任务)才会用到GCD。 可能是开发经验不够。
- 异步 + 并行或串行。 举个例子。
从子线程,异步返回主线程更新UI。 队列常用全局并行队列。 因为要下载图片耗时,而且具有网络不稳定性,所以放到子线程。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3948453733,2367168123&fm=27&gp=0.jpg"]]; UIImage *image = [UIImage imageWithData:imgData]; dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_async(mainQueue, ^{ UIImageView *imgView = [[UIImageView alloc]initWithImage:image]; [imgView setFrame:CGRectMake(0, 0, 200, 200)]; [imgView setCenter:self.view.center]; [self.view addSubview:imgView]; }); });复制代码
- 队列组 队列组能获知队列完成程度。 同时下载多个图片,所有图片下载完成之后去更新UI。
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent];// [self asyncSerial];// [self syncConcurrent]// [self syncSerial];// [self asyncMain];// [self syncMain]; [self groupTest]; NSLog(@"1---%@", [NSThread currentThread]);}- (void)groupTest { dispatch_queue_t conCurrentGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_group_t groupQueue = dispatch_group_create(); NSLog(@"current task"); dispatch_group_async(groupQueue, conCurrentGlobalQueue, ^{ NSLog(@"并行任务1"); }); dispatch_group_async(groupQueue, conCurrentGlobalQueue, ^{ NSLog(@"并行任务2"); }); dispatch_group_notify(groupQueue, mainQueue, ^{ NSLog(@"groupQueue中的任务 都执行完成,回到主线程更新UI"); }); NSLog(@"next task");}复制代码
dispatch_group_t groupQueue = dispatch_group_create();
用于生成队列组 2.生成队列时加上前缀_guoup 3. dispatch_group_notify
这个函数用以处理其他队列完成的块。 GCD其他的API
- dispatch_once:这个函数保证在应用程序执行中只执行一次指定处理的API。(见过用于音乐播放器单例)
static dispatch_once_ onceToken;dispatch_once( &onceToken,^{对象A =[ [对象A alloc] init];});复制代码
- dispatch_barrier_async:栅栏方法。用于在同一个队列中,阻断前后的任务。
- (void)viewDidLoad { [super viewDidLoad]; // [self asyncConcurrent];// [self asyncSerial];// [self syncConcurrent]// [self syncSerial];// [self asyncMain];// [self syncMain];// [self groupTest]; [self barrier]; NSLog(@"1---%@", [NSThread currentThread]);}// 栏栅函数- (void)barrier { dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"任务A---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务B---%@",[NSThread currentThread]); }); dispatch_barrier_async(queue, ^{ NSLog(@"栏栅函数---%@",[NSThread currentThread]); });// 换成同步执行也一样// dispatch_barrier_sync(queue, ^{// NSLog(@"栏栅函数---%@",[NSThread currentThread]);// }); dispatch_async(queue, ^{ NSLog(@"任务C---%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"任务D---%@",[NSThread currentThread]); });}复制代码
- dispatch_after:延时执行方法,时间并不精准。我常用其他延时方法,不展开谈论这个。
- dispatch_apply:快速迭代方法。 for必须按顺序同步遍历,dispatch_apply可以同时遍历多个数字。相当于开线程遍历。
dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_apply(10, queue, ^(size_t i) { NSLog(@"%zd----%@", i, [NSThread currentThread]; }复制代码
- 还有一些别的不常用就不展开了。
GCD会遇到的问题
- 死锁 上面解释过了
- 线程安全 场景:两条不同的线程之间同时对一个数据I/O。 比如商品数量 count = 10 , 单价price = 2 单件重量 = 0.1 A线程要取某个商品数量,算出商品总价,商品总重量。 B线程修改商品数量。 假如A先算出商品总价20,这时B突然修改了count = 11,那A算出的重量是1.1,而不是期望的10。 解决方法: 先简单理解线程和runloop。主线程必定会开一条runloop。但子线程默认是不开启的。开启了runloop就会执行某个机制,让线程在循环,不至于销毁。 所以我们可以在A访问到count时,对count加锁,别的线程只可以取值,不可以写入。这时别的线程如果访问不到,就会开启runloop,不定时访问,看看count解锁没有。
加锁方法
方法一 互斥锁(同步锁)
@synchronized(锁对象) { // 需要锁定的代码}复制代码
判断的时候锁对象要存在,如果代码中只有一个地方需要加锁,大多都使用self作为锁对象,这样可以避免单独再创建一个锁对象。 方法二:自旋锁 用到属性修饰原子属性nonatomic
和 atomic非原子属性
- atomic:保证同一时间只有一个线程能够写入,读取随意
- nonatomic:同一时间可以有很多线程读和写 atomic带有自旋锁,别的线程如果写入,就会开启runloop。 但是循环执行的线程,会消耗不少资源。所以一般开发中,除非确定不然不要用atomic。
参考
力荐第三篇,看了很多瞎说的,就这篇真实!!!