iOS开发之多线程编程总结(二)

背景

担心了两周的我终于轮到去医院做胃镜检查了!去的时候我都想好了最坏的可能(胃癌),之前在网上查的症状都很相似。最后检查结果出来终于安心了,诊断结果:慢性非萎缩性胃炎(胃窦为主)

我是一个心里素质不过关的人,所以说对待问题的时候可能会有一种悲观的想法。朋友说我本来可能没病都被自己吓出病了,这是一个心态问题。

你们可能问我做胃镜什么感觉?我只能告诉你一个字:真爽,具体只能自己去感受。

11970779-ed8e897d5826ecc9

自己眼中的自己.jpg

保持乐观的心态

看完下面的笑话就要开始我们的装逼之旅了^_^

  1. 一个大学生去公司实习,老板让他先从扫地开始。大学生:“我可是大学生哎……”老板:“哦,对了,我差点忘了你是大学生,来来来,我教你怎么扫地”
  2. 一天,老师让同学们写作文,题目是 《我的理想》。
    小明在作文里写道:我长大了要去抢银行,然后把钱分给穷苦老百姓。
    第二天老师改完了,写给小明的评语是这样的:很不错的理想,分钱的时候不要忘了老师,但你要注意你的同桌,他说他长大了要去当警察。

GCD基本介绍

  • GCD(Grand Central Dispatch)iOS 4.0开始引入的新多线程编程功能,
  • GCD(Grand Central Dispatch)是异步执行任务的技术之一。一般将应用程序中记述的线程管理用的代码在系统级中实现。开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。
  • GCD(Grand Central Dispatch)是基于C语言开发的一套多线程开发机制,是完全面向过程的。

GCD基本概念

这就需要上一篇博客里的基本知识了(不清楚去看下)iOS开发之多线程编程总结(一)

任务和队列

  • 任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的。执行任务有两种方式:同步执行异步执行。两者的主要区别是:是否具备开启新线程的能力。
    1. 同步执行(sync):只能在当前线程中执行任务,不具备开启新线程的能力
      • 必须等待当前语句执行完毕,才会执行下一条语句
      • 不会开启线程
      • 在当前主线程执行 block 的任务
      • dispatch_sync(queue, block);
    2. 异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力
      • 不用等待当前语句执行完毕,就可以执行下一条语句
      • 会开启线程执行 block 的任务
      • 异步是多线程的代名词
      • dispatch_async(queue, block);
  • 队列:这里的队列指任务队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。在GCD中有四种队列:串行队列并发队列主队列全局队列
    1. 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
      • 一次只能”调度”一个任务
      • dispatch_queue_create("queue", NULL);
        或者dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    2. 并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),
      • 一次可以”调度”多个任务
      • 并发功能只有在异步(dispatch_async)函数下才有效
      • dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    3. 主队列
      • 专门用来在主线程上调度任务的队列
      • 不会开启线程
      • 在主线程空闲时才会调度队列中的任务在主线程执行
      • dispatch_get_main_queue();
    4. 全局队列
      • 执行过程和并发队列一致,参考并发队列
      • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

小结: 在以后的使用中,记住下面的就可以了!

  1. 开不开线程由执行任务的函数决定
    • 异步开,异步是多线程的代名词
    • 同步不开
  2. 开几条线程由队列决定
    • 串行队列开一条线程(GCD会开一条,NSOperation Queue最大并发数为1时也可能开多条)
    • 并发队列开多条线程,具体能开的线程数量由底层线程池决定

GCD的使用

今天我们学习下面图片的相关知识点,Demo下载链接会在文章最后给出来

12970779-13fc7deb64f5ac76

GCD知识点.png

简单来看一段代码:异步下载图片

NSThread对比可以发现

  • 所有的代码写在一起的,让代码更加简单,易于阅读和维护
  • NSThread 通过 @selector 指定要执行的方法,代码分散
  • GCD 通过 block 指定要执行的代码,代码集中
  • 使用 GCD 不需要管理线程的创建/销毁/复用的过程!程序员不用关心线程的生命周期
  • 如果要开多个线程 NSThread 必须实例化多个线程对象或者使用分类方法
  • NSThread 靠 NSObject 的分类方法实现的线程间通讯,GCD 靠 block
  • dispatch_async(queue, block);就是异步执行一个队列里面的任务block。每个block之间是异步执行的,但是block里面的代码是顺序执行的!
  • dispatch_sync(queue, block);就是同步执行一个队列里面的任务block

1. 串行队列(Serial Dispatch Queue)

串行队列的创建:

串行队列同步和异步执行Demo:

串行队列 同步执行结果:

串行队列 异步执行结果:

小结:
  • 从串行队列同步执行结果看出打印是交替执行的!从打印中看到number=1,说明线程block任务是在主线程中执行的。因为同步是不会开辟线程的,所有当前只有一个主线程MainThread。也就是说串行队列同步执行不会开辟线程,所有block任务之间是同步执行的
  • 从串行队列异步执行结果看出打印并不是交替执行的!从打印中看到number=5,说明线程block的任务是在一个全新的线程中执行的。因为异步是会开辟线程的,所有当前有主线程MainThread和子线程number=5。也就是说串行队列异步执行会仅会开辟一个新的线程,所有block任务之间是同步执行的
  • 以先进先出的方式,顺序调度队列中的任务执行
  • 无论队列中指定的任务函数是同步还是异步,都会等待前一个任务执行完毕以后,再调度后面的任务

2. 并发队列(Concurrent Dispatch Queue)

并发队列创建

并发队列同步和异步执行Demo

并发 队列同步执行结果:

并发队列 异步执行结果:

小结:
  • 并发队列同步执行和串行队列同步执行一样,都不会开辟新线程,block任务之间是同步执行的!
  • 并发队列异步执行结果中看到开辟了多个线程,并且执行顺序也不是顺序执行。因为异步开多线程的代名词,并发是开多条线程的代名词
  • 有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理。
  • 以先进先出的方式,并发调度队列中的任务执行
  • 如果当前调度的任务是同步执行的,会等待任务执行完成后,再调度后续的任务
  • 如果当前调度的任务是异步执行的,同时底层线程池有可用的线程资源,会再新的线程调度后续任务的执行

3. 全局队列(Global Dispatch Queue)

全局队列基本知识
  • dispatch_get_global_queue函数来获取
  • 全局队列是所有应用程序都能够使用的并发队列(Concurrent Dispatch Queue),没必要通过dispatch_queue_create函数逐个生成并发队列,只需要获取Global Dispatch Queue即可
  • 是系统为了方便程序员开发提供的,其工作表现与并发队列一致其工作表现与并发队列一致其工作表现与并发队列一致
  • Global Dispatch Queue有4个优先级,分别是高优先级、默认优先级、低优先级、后台优先级!因为XNU内核用于Global Dispatch Queue的线程并不能保证实时性,因此执行优先级知识大概的判断和区分。
全局队列同步和异步执行Demo

全局队列 同步执行结果:

全局队列 异步执行结果:

4. 主队列(Main Dispatch Queue)

主队列基本知识
  • dispatch_get_main_queue()函数来获取
  • 专门用来在主线程上调度任务的队列
  • 不会开启线程
  • 以先进先出的方式,在主线程空闲时才会调度队列中的任务在主线程执行
  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度
主队列同步和异步执行Demo

主队列 同步执行结果:

主队列 异步执行结果:

5.延迟执行(dispatch_after)

  • 在GCD中我们使用dispatch_after()函数来延迟执行队列中的任务, dispatch_after()是异步执行队列中的任务的,也就是说使用dispatch_after()来执行队列中的任务不会阻塞当前任务。等到延迟时间到了以后就会开辟一个新的线程然后执行队列中的任务。
  • dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);来创建
  • 经过我的猜测测试(看不到源码),你们也思考一下这个延迟函数的实现过程。底层实现应该是dispath_async函数追加block到Main Dispatch Queue等相应队列,伪代码如下(以Main Dispatch Queue为例):
延迟执行(dispatch_after)Demo

注意:
  • dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);
  • 第二个参数:指定要追加处理的Dispatch Queue
  • 第三个参数:指定要执行处理的Block
  • 第一个参数:指定时间用的dispatch_time_t类型的值,该值是使用dispatch_time函数或者dispatch_walltime函数生成
    1.dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
    里面有两个参数:


    2.使用dispatch_walltime函数生成dispatch_time_t,用于计算绝对时间,例如在dispatch_after函数中指定2016年11月3日21时45分1秒这一绝对时间,可粗略的闹钟功能来使用。也就是到指定时间就会执行任务

6. 更改优先级(dispatch_set_target_queue)

  • dispatch_queue_create函数生成的Dispatch Queue不管是 串行队列(Serial Dispatch Queue) 还是 并发队列(Concurrent Dispatch Queue) ,都是用与 默认优先级全局队列(Global Dispatch Queue)相同执行优先级的线程。而全局队列工作方式与并发队列工作方式完全一致!
  • dispatch_set_target_queue(dispatch_object_t object,dispatch_queue_t _Nullable queue);函数来更改队列优先级
    • 第一个参数:指定要变更优先级的队列(要更改队列)
    • 第二个参数:指定第一个参数(队列)要和我们预期队列执行相同优先级的队列(目标队列)

1. dispatch_set_target_queue第一个Demo:

打印结果:

打印解释:
  • queue1和queue2设置的目标队列是全局队列(并发),也就是允许要更改的队列可以开辟多条线程(可以超过一条)
  • 代码中queue1变更和低优先级的全局队列一样优先级的串行队列,queue2是dispatch_queue_create创建和默认优先级的全局队列一样优先级的并发队列。
  • queue1和queue2都是异步执行,都会开辟线程,queue2是并发队列所以打印中看到有多个线程,并且任务之间也不是顺序执行。queue1是串行队列,所以只会开辟一个线程,任务会顺序执行。
  • queue2队列的优先级比queue1队列的优先级要高,从打印中可以看到queue2的确比queue1任务要普遍先执行。为什么要说普遍呢,因为从结果上看到queue1有一个任务是在queue2里任务没有执行完毕也执行了,为什么会出现这个问题呢??

2. dispatch_set_target_queue第一个Demo:

  • dispatch_set_target_queue除了能用来设置队列的优先级之外,还能够创建队列的执行层次
    ,当我们想让不同队列中的任务同步的执行时,我们可以创建一个串行队列,然后将这些队列的target指向新创建的队列即可,比如:

    13970779-013dbfc9d7364148

    队列的执行层次.png

打印结果:

打印解释:
  • queue1和queue2设置的目标队列是串行队列,也就是允许要更改的队列可以开辟一条线程。
  • queue1和queue2优先级一样,他两执行顺序相当于一个串行队列异步执行!
  • queue1和queue2队列以targetQueue队列为参照对象,那么queue1和queue2中的任务将按照targetQueue的队列处理。
  • 适用场景:一般都是把一个任务放到一个串行的queue中,如果这个任务被拆分了,被放置到多个串行的queue中,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。这时候dispatch_set_target_queue将起到作用。

dispatch_set_target_queue小结:

  • dispatch_set_target_queue可以更改Dispatch Queue优先级。
  • dispatch_set_target_queue可以更改队列的执行层次,队列里的任务将会按照目标队列(target Queue)的队列来处理

7. 任务组Dispatch Group

  • GCD的任务组在开发中是经常被使用到,当你一组任务结束后再执行一些操作时,使用任务组在合适不过了。dispatch_group的职责就是当队列中的所有任务都执行完毕后在去做一些操作,也就是说在任务组中执行的队列,当队列中的所有任务都执行完毕后就会发出一个通知来告诉用户任务组中所执行的队列中的任务执行完毕了。关于将队列放到任务组中执行有两种方式,一种是使用dispatch_group_async()函数,将队列与任务组进行关联并自动执行队列中的任务。另一种方式是手动的将队列与组进行关联然后使用异步将队列进行执行,也就是dispatch_group_enter()dispatch_group_leave()方法的使用。下方就给出详细的介绍。
1.队列与组自动关联并执行
  • 首先我们来介绍dispatch_group_async()函数的使用方式,该函数会将队列与相应的任务组进行关联,并且自动执行。当与任务组关联的队列中的任务都执行完毕后,会通过dispatch_group_notify()函数发出通知告诉用户任务组中的所有任务都执行完毕了。使用通知的方式是不会阻塞当前线程的,如果你使用dispatch_group_wait()函数,那么就会阻塞当前线程,直到任务组中的所有任务都执行完毕。

    打印结果:

  • 上面的函数就是使用dispatch_group_async()函数将队列与任务组进行关联并执行。首先我们创建了一个全局队列(并发),然后又创建了一个类型为dispatch_group_t的任务组group。使用dispatch_group_async()函数将两者进行关联并执行。使用dispatch_group_notify()函数进行监听group中队列的执行结果,如果执行完毕后,我们就在主线程中对结果进行处理。
  • dispatch_group_notify()函数有两个参数一个是发送通知的group,另一个是处理返回结果的队列。
  • dispatch_group_async(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);函数与dispatch_async函数相同,都追加block到指定的Dispatch Queue中,与dispatch_async不同的是指定生成的Dispatch Group为第一个参数。指定的block属于指定的Dispatch Group,不是Dispatch Queue,切记!
  • 无论向什么样的Dispatch Queue中追加处理,使用Dispatch Group都可监视这些处理执行的结束。一旦检测到所有处理执行结束,就可将结束的处理追加到Dispatch Queue中。这就是使用Dispatch Group的原因。
  • 在追加到Dispatch Group中的处理全部执行结束时,代码中使用的dispatch_group_notify函数会将执行的Block追加到Dispatch Queue中,将第一个参数指定为要监视的Dispatch Group。在追加到该Dispatch Group的全部处理执行结束时,将第三个参数的Block追加到第二个参数的Dispatch Queue中。在dispatch_group_notify函数中不管指定什么样的Dispatch Queue,属于Dispatch Group的全部处理在追加指定的Block时都已执行结束。
  • 另外,在Dispatch Group中也可以使用dispatch_group_wait函数仅等待全部处理执行结束。

  • dispatch_group_wait函数的第二个参数指定为等待时间(超时)。它属于dispatch_time_t类型的值。代码中使用的DISPATCH_TIME_FOREVER,意味着永久等待。只要属于Dispatch Group的操作尚未执行结束,就会一直等待,中途不能取消。

    指定等待时间为1微秒时,应做如下处理:

  • 如果dispatch_group_wait函数的返回值不为0,就意味着虽然经过了指定的时间,但属于Dispatch Group的某一个处理还在执行中。如果返回值为0,那么全部处理执行结束。当等待时间为DISPATCH_TIME_FOREVER,由dispatch_group_wait函数返回时,属于Dispatch Group的处理必定全部执行结束,因此返回值恒为0。
  • 这里的“等待”是什么意思?这意味着一旦调用dispatch_group_wait函数,该函数就处于调用的状态而不返回。即执行dispatch_group_wait函数的现在的线程(当前线程)停止。在经过dispatch_group_wait函数中指定的时间或属于指定Dispatch Group的处理全部执行结束之前,执行该函数的线程停止,属于阻塞状态。
  • 指定DISPACH_TIME_NOW,则不用任何等待即可判定属于Dispatch Group的处理是否执行结束。

  • 在主线程的RunLoop的每次循环中,可检查执行是否结束,从而不消耗多余的等待时间,虽然这样有额可以,但一般在这种情况下,还是推荐用dispatch_group_notify函数追加结束处理到Main Dispatch Queue中。这是因为dispatch_group_notify函数可以简化代码。并且如果你用了diaptach_group_wait等待时间过长,中间不能取消队列任务这就很坑了!
2. 队列与组手动关联并执行
  • 接下来我们将手动的管理任务组与队列中的关系,也就是不使用dispatch_group_async()函数。我们使用dispatch_group_enter()dispatch_group_leave()函数将队列中的每次任务加入到到任务组中。首先我们使用dispatch_group_enter()函数进入到任务组中,然后异步执行队列中的任务,最后使用dispatch_group_leave()函数离开任务组即可。下面的函数中我们使用了dispatch_group_wait()函数,该函数的职责就是阻塞当前线程,来等待任务组中的任务执行完毕。该函数的第一个参数是所要等待的group,第二个参数是等待超时时间,此处我们设置的是DISPATCH_TIME_FOREVER,就说明等待任务组的执行永不超时,直到任务组中所有任务执行完毕。

打印结果:dispatch_group_wait()函数下方的print()函数在所有任务执行完毕之前是不会被调用的,因为dispatch_group_wait()会将当前线程进行阻塞。当然虽然是手动的将队列与任务组进行关联的,感觉display_group_notify()函数还是好用的。

8. 栅栏任务Dispatch_barrier_async

  • barrier顾名思义栅栏、障碍物的意思!
    在访问数据库或文件时,使用Serial Dispatch Queue可避免数据竞争的问题。

    写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。

    也就是说,为了高效率地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入出路在任何一个读取处理没有执行的状态下,追加到Serial Dispatch Queue中即可(在写入处理结束之前,读取处理不可执行)。

    虽然利用Dispatch Group和dispatch_set_target_queue函数也可实现,但代码会很复杂。有兴趣的可以自己尝试写写!
    CD为我们提供了更为聪明的解决办法——dispatch_barrier_async函数。该函数同dispatch_queue_create函数生成的Concurrent Dispatch Queue一起使用。

打印结果:

  • 使用Concurrent Dispatch Queuedispatch_barrier_async函数可实现高效率的数据库访问和文件访问,dispatch_barrier_async函数还是比较好理解的。

9. 循环执行dispatch_apply

  • dispatch_apply()函数是用来循环来执行队列中的任务的,使用方式为:dispatch_apply(循环次数, 任务所在的队列) { 要循环执行的任务 }。使用该函数循环执行并行队列中的任务时,会开辟新的线程,不过有可能会在当前线程中执行一些任务。而使用dispatch_apply()执行串行队列中的任务时,会在当前线程中执行。无论是使用并行队列还是串行队列,dispatch_apply()都会阻塞当前执行函数线程。

  • 输出结果中最后的done必定在最后的位置上,这是因为dispatch_apply函数会等待全部处理执行结束,也就是阻塞当前执行函数线程。
  • 另外,由于dispatch_apply函数也与dispatch_sync函数相同,会等待处理执行结束(阻塞),因此推荐在dispatch_async函数中非同步地执行dispatch_apply函数。

打印结果:

10. 队列的挂起和唤醒

  • 队列的挂起与唤醒相对较为简单,如果你想对一个队列中的任务的执行进行挂起,那么你就使用dispatch_suspend()函数即可。如果你要唤醒某个挂起的队列,那么你就可以使用dispatch_resum()函数。这两个函数所需的参数都是你要挂起或者唤醒的队列,鉴于知识点的简单性就不做过多的赘述了。

打印结果:可以先思考一下打印结果

GCD总结

  1. 开不开线程由执行任务的函数决定
    • 异步开,异步是多线程的代名词
    • 同步不开
  2. 开几条线程由队列决定
    • 串行队列开一条线程(GCD会开一条,NSOperation Queue最大并发数为1时也可能开多条)
    • 并发队列开多条线程,具体能开的线程数量由底层线程池决定
  3. GCD的使用步骤
    • 1.创建block任务
    • 2.创建GCD队列(串行和并发)
    • 3.把block任务放到GCD队列里,以异步或者同步方式执行GCD队列

结尾:

能看到这里的小伙伴都是真爱啊,内容太多了。学完这个你们多思考一下里面方法的联系以及实现,好了今天的GCD就讲到这里,中间有什么错误欢迎你们批评指出!下一篇博客将带你走进NSOperation。

喜欢的话就请点个喜欢加个关注^_^(楼主好无耻啊)❤️

ThreadDemo下载链接

参考书籍:Objective-C高级编程iOS与OS X多线程和内存管理

1 6 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部