美文网首页
iOS GCD入门和GCD对CPU多核的使用

iOS GCD入门和GCD对CPU多核的使用

作者: Hsusue | 来源:发表于2018-07-29 22:30 被阅读254次

前言

好开心

明天要给师弟开分享会,分享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 == 0
dispatch_queue_t globalQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//HIGH
dispatch_queue_t globalQueue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//LOW
dispatch_queue_t globalQueue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//BACKGROUND
dispatch_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, ^{
  // 这里放异步执行任务代码
  });

个人认为易迷惑的点

  • 太多的组合方式
    有两种任务执行方式,两种队列+特殊的主队列,就可以组成六种组合。
    有两张图总结得特别好,记住这两张图,分析的时候用得到。
    然后为了更好理解,自己也花了点时间弄了动图。
image.png image.png

还是不能忘了《Inside》的例子。

  • 两种待办任务表(对应队列)
    一种是多个机器人对多个宝藏,先入先出发。(对应并行队列)
    另一种是一个机器人对有顺序找的多个宝藏。(对应串行队列)
    特殊的 强行自己去做的任务表。 (对应主队列)

  • 你有两类事情(对应任务)
    一类是吃喝拉撒,一有需要就自己马上去做,总不能懒到让机器人帮忙吧。(对应着同步执行任务)
    另一类是寻宝,要机器人去做,出发前要点时间给机器人充电。(对应着异步执行任务)

代码中, 输出@"1"对应着吃喝拉撒

  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---");
}
多个机器人找多个宝藏 异步 + 并行队列(代码一开始是任务123)

多个机器人找多个宝藏,完成程度和你的吃喝拉撒没必然先后顺序。

  1. 异步 + 串行队列 (一个机器人找有序宝藏)
- (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---");
}
一个机器人找有序宝藏 异步 + 串行队列

一个机器人等有序宝藏图拼接好后,就出发了。和你吃喝拉撒没先后顺序。

  1. 同步 + 并行队列 (自己吃喝拉撒 放到 多个机器人对多个宝藏,准备好后机器人一起出发)
- (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---");
}
自己吃喝拉撒 放到 多个机器人对多个宝藏,准备好后机器人一起出发 同步 + 并行队列

一要吃喝拉撒就自己马上去做。所以不等@“end”输出就先做完了。最后再@“1”。有着必然先后顺序。

  1. 同步+ 串行队列 (自己吃喝拉撒 放到 一个机器人对有顺序找的多个宝藏)
- (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”。有着必然先后顺序。

  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---");
}

和异步 + 串行队列区别就是不开启新线程。


异步 + 主队列

让机器人充电准备,所以自己先吃喝拉撒完。直到@"1"。
然后你发现这件事是在强制自己做的任务表上,于是就自己一件接一件做了。

  1. 同步+主队列(死锁)(吃喝拉撒 + 强行自身去做)
- (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

// 调用时记住进入地址为1
dispatch_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");
}
队列组测试
1.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]);
    });
}
栏栅函数

可以这么理解


image.png
  • 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作为锁对象,这样可以避免单独再创建一个锁对象。
方法二:自旋锁
用到属性修饰原子属性nonatomicatomic非原子属性

  • atomic:保证同一时间只有一个线程能够写入,读取随意
  • nonatomic:同一时间可以有很多线程读和写
    atomic带有自旋锁,别的线程如果写入,就会开启runloop。
    但是循环执行的线程,会消耗不少资源。所以一般开发中,除非确定不然不要用atomic。

参考

力荐第三篇,看了很多瞎说的,就这篇真实!!!

相关文章

  • iOS GCD入门和GCD对CPU多核的使用

    前言 明天要给师弟开分享会,分享GCD。好方,只理解一些皮毛拿什么去装。准备的时候顺便把过程记录下来。 目录 概念...

  • GCD 小总结

    简介:GCD是对多线程、多核开发较完整的封装。在使用GCD的时候,系统会自动根据CPU使用情况进行调度,所以GCD...

  • 2021-02-20

    GCD GCD是iOS4.0 推出的,主要针对多核cpu做了优化,是C语言的技术GCD是将任务(block)添加到...

  • iOS GCD总结

    1.为什么要使用GCD? ·GCD可用于多核的并行运算 ·GCD会自动利用更多的CPU内核(双核 四核) ·GCD...

  • GCD的理解与使用

    原文链接 GCD的特点 • GCD会自动利用更多的CPU内核• GCD 可用于多核的并行运算• GCD...

  • 浅析GCD

    GCD目录: 1. GCD简介 为什么要用GCD呢? GCD可用于多核的并行运算GCD会自动利用更多的CPU内核(...

  • GCD复习

    GCD的优势: GCD 可用于多核的并行运算 GCD 会自动利用更多的 CPU 内核(比如双核、四核) GCD会自...

  • 线程的相关总结GCD

    GCD好处GCD 可用于多核的并行运算GCD 会自动利用更多的 CPU 内核(比如双核、四核)GCD 会自动管理线...

  • GCD的探索三两事

    GCD GCD优势 GCD是苹果公司为多核的并行运算提出的解决方案 GCD会自动利用更多的CPU内核(比如双核、四...

  • GCD笔记

    一 GCD 的好处:1)GCD 可用于多核的并行运算2)GCD 会自动利用更多的 CPU 内核(比如双核、四核)...

网友评论

      本文标题:iOS GCD入门和GCD对CPU多核的使用

      本文链接:https://www.haomeiwen.com/subject/hulzmftx.html