美文网首页
iOS之武功秘籍⑬: 多线程原理与GCD和NSOperation

iOS之武功秘籍⑬: 多线程原理与GCD和NSOperation

作者: 長茳 | 来源:发表于2021-03-01 23:43 被阅读0次

iOS之武功秘籍 文章汇总

写在前面

多线程在iOS中有着举足轻重的地位,那么本篇文章就来带你全面走进她.....

本节可能用到的秘籍Demo

一、基本概念及原理

① 线程、进程与队列

①.1 线程的定义

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程想要执行任务,必须得有线程,进程至少要有一条线程
  • 程序启动会默认开启一条线程,这条线程被成为主线程UI线程

①.2 进程的定义

  • 进程是指在系统中正在运行的一个应用程序,如微信、支付宝app都是一个进程
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
  • 通过“活动监视器”可以查看mac系统中所开启的线程

所以,可以简单的理解为:进程是线程的容器,而线程用来执行任务.在iOS中是单进程开发,一个进程就是一个app,进程之间是相互独立的,如支付宝、微信、qq等,这些都是属于不同的进程.

①.3 进程与线程的关系和区别

  • 地址空间:同一进程线程共享本进程的地址空间,而进程之间则是独立的地址空间
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的
  • 两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即进程相当于一个工厂,线程相当于工厂中的一条流水线
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮
  • 进程切换时,消耗的资源大、效率高.所以设计到频繁的切换时,使用线程要好于进程.同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程而不能用进程
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口.但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
  • 线程是处理器调度的基本单位,但进程不是
  • 线程没有地址空间,线程包含在进程地址空间中

可能会觉得这些理论知识很抽象,百度出来一大堆但是都不好理解,看完下面的理解就全明白了

①.4 进程与线程的关系图

可以把iOS系统想象成商场进程则是商场中的店铺线程是店铺雇佣的员工

  • 进程之间的相互独立
    • 奶茶店看不到果汁店的账目(访问不了别的进程的内存)
    • 果汁店用不了奶茶店的波霸(进程之间的资源是独立的)
  • 进程至少要有一条线程
    • 店铺至少要有一个员工(进程至少有一个线程)
    • 早上开店门的员工(相当于主线程)
  • 进程/线程崩溃的情况
    • 奶茶店倒闭了并不会牵连果汁店倒闭(进程崩溃不会对其他进程产生影响)
    • 奶茶店的收银员不干了会导致奶茶店无法正常运作(线程崩溃导致进程瘫痪)

移动开发不一定是单进程处理的,android就是多进程处理的;而iOS采用沙盒机制,这也是苹果运行能够流畅安全的一个主要原因

①.5 线程和runloop的关系

  • runloop与线程是一一对应的 —— 一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里
  • runloop是来管理线程的 —— 当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务
  • runloop在第一次获取时被创建,在线程结束时被销毁
    • 对于主线程来说,runloop在程序一启动就默认创建好了
    • 对于子线程来说,runloop是懒加载的 —— 只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调

①.6 影响任务执行速度的因素

以下因素都会对任务的执行速度造成影响:

  • cpu的调度
  • 线程的执行速率
  • 队列情况
  • 任务执行的复杂度
  • 任务的优先级

② 多线程

②.1 多线程原理

  • 对于单核CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作(执行)
  • iOS中的多线程同时执行的本质CPU在多个任务之间进行快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果.其中切换的时间间隔就是时间片

②.2 多线程意义

优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU、内存)
  • 线程上的任务执行完成后,线程会自动销毁

缺点

  • 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占512KB,创建线程大约需要90毫秒的创建时间)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

②.3 多线程生命周期

多线程的生命周期主要分为5部分:新建 - 就绪 - 运行 - 阻塞 - 死亡,如下图所示

  • 新建:主要是实例化线程对象
  • 就绪:线程对象调用start方法,将线程对象加入可调度线程池等待CPU的调用,即调用start方法,并不会立即执行,进入就绪状态,需要等待一段时间,经CPU调度后才执行,也就是从就绪状态进入运行状态
  • 运行CPU 负责调度可调度线程池中线程的执行.在线程执行完成之前,其状态可能会在就绪和运行之间来回切换.就绪和运行之间的状态变化由CPU负责,程序员不能干预.
  • 阻塞:当满足某个预定条件时,可以使用休眠或锁,阻塞线程执行.sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)
  • 死亡:分为两种情况:正常死亡,即线程执行完毕. 非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)

简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片

  • 如果时间片用尽,线程就会进入就绪状态队列
  • 如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列
  • 等待事件发生后,线程又会重新进入就绪状态队列
  • 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列选择一个线程继续执行

线程的exitcancel说明

  • exit:一旦强行终止线程,后续的所有代码都不会执行
  • cancel:取消当前线程,但是不能取消正在执行的线程

那么是不是线程的优先级越高,意味着任务的执行越快?
并不是,线程执行的快慢,除了要看优先级,还需要查看资源的大小(即任务的复杂度)、以及 CPU 调度情况.在NSThread中,线程优先级threadPriority已经被服务质量qualityOfService取代,以下是相关的枚举值

②.4 线程池的原理

  • 【第一步】判断核心线程池是否都正在执行任务

    • 返回NO,创建新的工作线程去执行
    • 返回YES,进入【第二步】
  • 【第二步】判断线程池工作队列是否已经饱满

    • 返回NO,将任务存储到工作队列,等待CPU调度
    • 返回YES,进入【第三步】
  • 【第三步】判断线程池中的线程是否都处于执行状态

    • 返回NO,安排可调度线程池中空闲的线程去执行任务
    • 返回YES,进入【第四步】
  • 【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)

    • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行
    • CallerRunsPolicy:将任务回退到调用者
    • DisOldestPolicy:丢掉等待最久的任务
    • DisCardPolicy:直接丢弃任务

②.5 iOS中多线程的实现方案

iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation,汇总如图所示

下面是以上四种方案的简单示例

C和OC的桥接
其中涉及C与OC的桥接,有以下几点说明

  • __bridge只做类型转换,但是不修改对象(内存)管理权
  • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象
  • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC

②.6 线程安全问题

当多个线程同时访问一块资源时,容易引发数据错乱和数据安全问题,有以下两种解决方案

  • 互斥锁(即同步锁):@synchronized
  • 自旋锁
②.6.1 互斥锁 vs 自旋锁

互斥锁

  • 保证锁内的代码,同一时间,只有一条线程能够执行!
  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
  • 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠
  • 能够加锁的任意 NSObject 对象
  • 注意:锁对象一定要保证所有的线程都能够访问
  • 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象

自旋锁

  • 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态
  • 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本身就有一把自旋锁
  • 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能

考考你: 自旋锁vs互斥锁的区别?

  • 相同点:在同一时间,保证了只有一条线程执行任务,即保证了相应同步的功能

  • 不同点:

    • 互斥锁:发现其他线程执行,当前线程 休眠(即就绪状态),进入等待执行,即挂起.一直等其他线程打开之后,然后唤醒执行
    • 自旋锁:发现其他线程执行,当前线程 忙等(即一直访问),处于忙等状态,耗费的性能比较高
  • 使用场景:根据任务复杂度区分,使用不同的锁

    • 当前的任务状态比较短小精悍时,用自旋锁
    • 反之的,用互斥锁
②.6.2 atomic与nonatomic 的区别

atomicnonatomic主要用于属性的修饰,以下是相关的一些说明

  • nonatomic 非原子属性
  • atomic 原子属性(线程安全),针对多线程设计的,默认值
    • 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
    • atomic 本身就有一把锁(自旋锁)
    • 单写多读:单个线程写入,多个线程可以读取
  • atomic:线程安全,需要消耗大量的资源
  • nonatomic:非线程安全,适合内存小的移动设备

iOS 开发的建议

  • 所有属性都声明为 nonatomic
  • 尽量避免多线程抢夺同一块资源 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

②.7 线程间通讯

Threading Programming Guide文档中,提及,线程间的通讯有以下几种方式
[图片上传失败...(image-a8fb76-1614613062037)]

  • 直接消息传递: 通过performSelector的一系列方法,可以实现由某一线程指定在另外的线程上执行任务.因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化
  • 全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块.尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱.必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性. 否则可能会导致竞争状况,数据损坏或崩溃。
  • 条件执行: 条件是一种同步工具,可用于控制线程何时执行代码的特定部分.您可以将条件视为关守,让线程仅在满足指定条件时运行.
  • Runloop sources: 一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息.由于 Runloop source 是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率
  • Ports and sockets:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术.更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信.为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态.需要注意的是,端口通讯需要将端口加入到主线程的Runloop中,否则不会走到端口回调方法
  • 消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据.尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效
  • Cocoa 分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实.尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销.分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高.

②.8 GCD和NSOperation的比较

  • GCDNSOperation的关系如下:

    • GCD是面向底层的C语言的API
    • NSOperation是用GCD封装构建的,是GCD的高级抽象
  • GCD和NSOperation的对比如下:

    • GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构 —— 写起来更加方便
    • GCD只支持FIFO的队列,而NSOpration可以设置最大并发数、设置优先级、添加依赖关系等调整执行顺序
    • NSOpration甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier任务才能控制执行顺序,较为复杂
    • NSOperation支持KVO(面向对象)可以检测operation是否正在执行、是否结束、是否取消

二、NSthread

NSthread是苹果官方提供面向对象的线程操作技术,是对thread的上层封装,比较偏向于底层.简单方便,可以直接操作线程对象,使用频率较少.

① 创建线程

线程的创建方式主要有以下三种方式

  • 通过init初始化方式创建
  • 通过detachNewThreadSelector构造器方式创建
  • 通过performSelector...方法创建,主要是用于获取主线程,以及后台线程

② 属性

③ 类方法

常用的类方法有以下几个

  • currentThread:获取当前线程
  • sleep...:阻塞线程
  • exit:退出线程
  • mainThread:获取主线程

三、GCD

① GCD简介

什么是GCD?
GCD全称是Grand Central Dispatch,它是纯 C 语言,并且提供了非常多强大的函数

GCD的优势:

  • GCD 是苹果公司为多核的并行运算提出的解决方案
  • GCD自动利用更多的CPU内核(比如双核、四核)
  • GCD自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

用一句话总结GCD就是:将任务添加到队列,并且指定执行任务的函数

② GCD核心

在日常开发中,GCD一般写成下面这种形式

将上述代码拆分,方便我们来理解GCD的核心,主要是由 任务 + 队列 + 函数 构成

  • 使用dispatch_block_t创建任务
  • 使用dispatch_queue_t创建队列
  • 将任务添加到队列,并指定执行任务的函数dispatch_async

注意
这里的任务是指执行操作的意思,在使用dispatch_block_t创建任务时,主要有以下两点说明

  • 任务使用block封装
  • 任务的block没有参数也没有返回值

③ 函数与队列

③.1 函数

GCD中执行任务的方式有两种,同步执行异步执行,分别对应同步函数dispatch_sync异步函数dispatch_async,两者对比如下

  • 同步执行,对应同步函数dispatch_sync
    • 必须等待当前语句执行完毕,才会执行下一条语句
    • 不会开启线程,即不具备开启新线程的能力
    • 在当前线程中执行block任务
  • 异步执行,对应异步函数dispatch_async
    • 不用等待当前语句执行完毕,就可以执行下一条语句
    • 会开启线程执行block任务,即具备开启新线程的能力(但并不一定开启新线程,这个与任务所指定的队列类型有关)
    • 异步是多线程的代名词

综上所述,两种执行方式的主要区别有两点:

  • 是否等待队列的任务执行完毕
  • 是否具备开启新线程的能力

③.2 队列

多线程中所说的队列Dispatch Queue)是指执行任务的等待队列,即用来存放任务的队列.队列是一种特殊的线性表,遵循先进先出(FIFO)原则,即新任务总是被插入到队尾,而任务的读取从队首开始读取.每读取一个任务,则动队列中释放一个任务,如下图所示

③.2.1 串行队列 和 并发队列

GCD中,队列主要分为串行队列(Serial Dispatch Queue)并发队列(Concurrent Dispatch Queue)两种,如下图所示

  • 串行队列:每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个,即只开启一个线程(通俗理解:同一时刻只调度一个任务执行)
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);创建串行队列
    • 其中的DISPATCH_QUEUE_SERIAL也可以使用NULL表示,这两种均表示 默认的串行队列
  • 并发队列:一次可以并发执行多个任务,即开启多个线程,并同时执行任务(通俗理解:同一时刻可以调度多个任务执行)
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);创建并发队列
    • 注意:并发队列的并发功能只有在异步函数下才有效
③.2.2 主队列 和 全局并发队列

GCD中,针对上述两种队列,分别提供了主队列(Main Dispatch Queue)全局并发队列(Global Dispatch Queue)

  • 主队列Main Dispatch Queue):GCD中提供的特殊的串行队列
    • 专门用来在主线程上调度任务的串行队列,依赖于主线程主Runloop,在main函数调用之前自动创建
    • 不会开启线程
    • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度
    • 使用dispatch_get_main_queue()获得主队列
    • 通常在返回主线程更新UI时使用
  • 全局并发队列Global Dispatch Queue):GCD提供的默认的并发队列
  • 为了方便程序员的使用,苹果提供了全局队列
  • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列
  • 使用dispatch_get_global_queue获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)
    • 第一个参数表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已经被服务质量(quality-of-service)取代
    • 第二个参数使用0
③.2.3 全局并发队列 + 主队列 配合使用

在日常开发中,全局队列+并发并列一般是这样配合使用的

③.3 函数与队列的不同组合

主队列和全局队列单独考虑,组合结果以总结表格为准

③.3.1 串行队列 + 同步函数

任务一个接一个的在当前线程执行,不会开辟新线程

③.3.2 串行队列 + 异步函数

任务一个接一个的执行,会开辟新线程

③.3.3 并发队列 + 同步函数
任务一个接一个的执行,不开辟线程
③.3.4 并发队列 + 异步函数

任务乱序执行,会开辟新线程

③.3.5 主队列 + 同步函数

任务相互等待造成死锁

造成死锁的原因分析如下:

  • 主队列有两个任务,顺序为:CJNSLog任务 - 同步block
  • 执行CJNSLog任务后,执行同步Block,会将任务1(即i=1时)加入到主队列,主队列顺序为:CJNSLog任务 - 同步block - 任务1
  • 任务1的执行需要等待同步block执行完毕才会执行,而同步block的执行需要等待任务1执行完毕,所以就造成了任务互相等待的情况,即造成死锁崩溃

死锁现象

  • 主线程因为你同步函数的原因等着先执行任务
  • 主队列等着主线程的任务执行完毕再执行自己的任务
  • 主队列和主线程相互等待会造成死锁
③.3.6 主队列 + 异步函数

任务一个接一个的执行,不开辟线程

③.3.7 全局并发队列 + 同步函数

任务一个接一个的执行,不开辟新线程

③.3.8 全局并发队列 + 异步函数

任务乱序执行,会开辟新线程

③.3.9 总结
函数与队列 串行队列 并发队列 主队列 全局并发队列
同步函数 顺序执行,不开辟线程 顺序执行,不开辟线程 死锁 顺序执行,不开辟线程
异步函数 顺序执行,开辟线程 乱序执行,开辟线程 顺序执行,不开辟线程 乱序执行,开辟线程

④ dispatch_after

⑤ dispatch_once

⑥ dispatch_apply

⑦ dispatch_group_t

dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间
应用场景:多个接口请求之后刷新页面
有以下两种使用方式

⑦.1 使用dispatch_group_async + dispatch_group_notify

dispatch_group_notifydispatch_group_async执行结束之后会受收到通知

⑦.2 使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify

dispatch_group_enterdispatch_group_leave成对出现,使进出组的逻辑更加清晰

调度组要注意搭配使用,必须先进组再出组,缺一不可

⑦.3 在⑦.2 的基础上使用 dispatch_group_wait

⑧ dispatch_barrier_sync & dispatch_barrier_async

栅栏函数,主要有两种使用场景:串行队列、并发队列.
应用场景:同步锁

等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中.
简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务.

⑧.1 串行队列使用栅栏函数

不使用栅栏函数


使用栅栏函数


栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1任务2

结论:由于串行队列异步执行任务是一个接一个执行完毕的,所以使用栅栏函数没意义

⑧.2 并发队列使用栅栏函数

不使用栅栏函数


使用栅栏函数


结论:由于并发队列异步执行任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序

⑧.3 dispatch_barrier_sync/dispatch_barrier_async区别
  • dispatch_barrier_async:前面的任务执行完毕才会来到这里
  • dispatch_barrier_sync:作用相同,但是这个会堵塞线程,影响后面的任务执行

将案例二中的dispatch_barrier_async改成dispatch_barrier_sync

结论:dispatch_barrier_async可以控制队列中任务的执行顺序,而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)

⑧.4 栅栏函数注意点
  • 1.尽量使用自定义的并发队列
    • 使用全局队列起不到栅栏函数的作用
    • 使用全局队列时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)
  • 2.栅栏函数只能控制同一并发队列:打个比方,平时在使用AFNetworking做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列

⑨ dispatch_semaphore_t

信号量主要用作同步锁,用于控制GCD最大并发数

  • dispatch_semaphore_create():创建信号量
  • dispatch_semaphore_wait():等待信号量,信号量减1.当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到信号(signal)才执行下去
  • dispatch_semaphore_signal():释放信号量,信号量加1.当信号量>= 0 会执行wait之后的代码.

下面这段代码要求使用信号量来按序输出(当然栅栏函数可以满足要求)


利用信号量的API来进行代码改写

如果当创建信号量时传入值为1又会怎么样呢?

  • i=0时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为0不会阻塞线程,所以进入i=1
  • i=1时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为-1阻塞线程,等待signal再执行下去

结论:

  • 创建信号量时传入值为1时,可以通过两次才堵塞
  • 传入值为2时,可以通过三次才堵塞

⑩ dispatch_source

dispatch_source_t主要用于计时操作,其原因是因为它创建的timer不依赖于RunLoop,且计时精准度比NSTimer

⑩.1 定义及使用

dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件

  • Timer Dispatch Source:定时器事件源,用来生成周期性的通知或回调
  • Signal Dispatch Source:监听信号事件源,当有UNIX信号发生时会通知
  • Descriptor Dispatch Source:监听文件或socket事件源,当文件或socket数据发生变化时会通知
  • Process Dispatch Source:监听进程事件源,与进程相关的事件通知
  • Mach port Dispatch Source:监听Mach端口事件源
  • Custom Dispatch Source:监听自定义事件源

主要使用的API:

  • dispatch_source_create: 创建事件源
  • dispatch_source_set_event_handler: 设置数据源回调
  • dispatch_source_merge_data: 设置事件源数据
  • dispatch_source_get_data: 获取事件源数据
  • dispatch_resume: 继续
  • dispatch_suspend: 挂起
  • dispatch_cancle: 取消
⑩.2 自定义定时器

在iOS开发中一般使用NSTimer来处理定时逻辑,但NSTimer是依赖Runloop的,而Runloop可以运行在不同的模式下.如果NSTimer添加在一种模式下,当Runloop运行在其他模式下的时候,定时器就挂机了;又如果Runloop在阻塞状态,NSTimer触发时间就会推迟到下一个Runloop周.。因此NSTimer在计时上会有误差,并不是特别精确,而GCD定时器不依赖Runloop,计时精度要高很多

使用dispatch_source自定义定时器注意点:

  • GCDTimer需要强持有,否则出了作用域立即释放,也就没有了事件回调
  • GCDTimer默认是挂起状态,需要手动激活
  • GCDTimer没有repeat,需要封装来增加标志位控制
  • GCDTimer如果存在循环引用,使用weak+strong或者提前调用dispatch_source_cancel取消timer
  • dispatch_resumedispatch_suspend调用次数需要平衡
  • source挂起状态下,如果直接设置source = nil或者重新创建source都会造成crash.正确的方式是在激活状态下调用dispatch_source_cancel(source)释放当前的source

四、NSOperation

NSOperation是个抽象类,依赖于子类NSInvocationOperationNSBlockOperation去实现

下面是开发者文档上对NSOperation的一段描述

① NSInvocationOperation

  • 基本使用


  • 直接处理事务,不添加隐性队列


  • 接下来就会引申出下面一段错误使用代码


上述代码之所以会崩溃,是因为线程生命周期:

  • queue addOperation:op已经将处理事务的操作任务加入到队列中,并让线程运行
  • op start将已经运行的线程再次运行会造成线程混乱

② NSBlockOperation

NSInvocationOperationNSBlockOperation两者的区别在于:

  • 前者类似target形式
  • 后者类似block形式——函数式编程,业务逻辑代码可读性更高

NSOperationQueue是异步执行的,所以任务一任务二的完成顺序不确定

通过addExecutionBlock这个方法可以让NSBlockOperation实现多线程

③ 自定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务

④ NSOperationQueue

NSOperationQueue有两种队列:主队列、其他队列.其他队列包含了 串行和并发.

  • 主队列:主队列上的任务是在主线程执行的
  • 其他队列(非主队列):加入到非主队列中的任务默认就是并发,开启多线程
    例如我们在 ② NSBlockOperation 中说的那样.

⑤ 执行顺序

下列代码可以证明操作与队列的执行效果是异步并发

⑥ 设置优先级

NSOperation设置优先级只会让CPU有更高的几率调用,不是说设置高就一定全部先完成

  • 不使用sleep——高优先级的任务一先于低优先级的任务二

  • 使用sleep进行延时——高优先级的任务一慢于低优先级的任务二

⑦ 设置并发数

  • GCD中只能使用信号量来设置并发数
  • NSOperation轻易就能设置并发数
    • 通过设置maxConcurrentOperationCount来控制单次出队列去执行的任务数

⑧ 添加依赖

NSOperation中添加依赖能很好的控制任务执行的先后顺序

⑨ 线程间通讯

  • GCD中使用异步进行网络请求,然后回到主线程刷新UI
  • NSOperation中也有类似在线程间通讯的操作

⑩ 任务的挂起、继续、取消

但是在使用中经常会遇到一些匪夷所思的问题——明明已经挂起了任务,可还是继续执行了几个任务才停止执行

这幅图是并发量为2的情况:

  • 挂起前:任务3任务4等待被调度
  • 挂起瞬间:任务3任务4已经被调度出队列,准备执行,此时它们是无法挂起的
  • 挂起后:任务3任务4被线程执行,而原来的队列被挂起不能被调度

五、GCD底层分析

由于源码的篇幅较大、逻辑分支、宏定义较多,使得源码变得晦涩难懂,让开发者们望而却步.但如果带着疑问、有目的性的去看源码,就能减少难度,忽略无关的代码.首先提出我们要分析的几个问题:

  • 队列创建
  • 异步函数
  • 同步函数
  • 单例的原理
  • 栅栏函数的原理
  • 信号量的原理
  • 调度组的原理

① 源码的出处

分析源码首先得获取到GCD源码,之前已经分析过objcmallocdyld源码,那么GCD内容是在哪份源码中呢?

这里分享一个小技巧,由于已知要研究GCD,所以有以下几种选择源码的方法

  • Baidu/Google
  • 下符号断点dispatch_queue_createdispatch_async,打开汇编调式Debug->Debug Workflow->Always show Disassembly

这样子就找到了我们需要的libdispatch-1271.40.12源码

② 队列创建

通过前面的学习我们知道队列的创建是通过GCD中的dispatch_queue_create方法创建的,因此可以在源码中搜索dispatch_queue_create.
假如我们就直接搜索dispatch_queue_create的话,会出现众多的情况(66 results in 18 files),这时候就考验一个开发者阅读源码的经验了

在此,我们就要改一改搜索条件了:

  • 由于创建队列代码为dispatch_queue_create("XXX", NULL),所以搜索dispatch_queue_create( —— 将筛选结果降至(21 results in 6 files)
  • 由于第一个参数为字符串,在c语言中用const修饰,所以搜索dispatch_queue_create(const —— 将筛选结果降至(2 results in 2 files)

②.1 dispatch_queue_create

常规中间层封装 —— 便于代码迭代不改变上层使用


有时候也需要注意下源码中函数中的传参:

  • 此时label是上层的逆序全程域名,主要用在崩溃调试
  • attrNULL/DISPATCH_QUEUE_SERIAL、DISPATCH_QUEUE_CONCURRENT,用于区分队列是异步还是同步的

#define DISPATCH_QUEUE_SERIAL NULL 串行队列的宏定义其实是个NULL

②.2 _dispatch_lane_create_with_target

  • 1.通过_dispatch_queue_attr_to_info方法传入dqa(即队列类型,串行、并发等)创建dispatch_queue_attr_info_t类型的对象dqai,用于存储队列的相关属性信息

    • dispatch_queue_attr_info_tisa一样,是个位域结构,用于存储队列的相关属性信息
  • 2.设置队列相关联的属性,例如服务质量qos等
  • 3.通过DISPATCH_VTABLE拼接队列名称,即vtable,其中DISPATCH_VTABLE是宏定义,如下所示,所以队列的类型是通过OS_dispatch_+队列类型queue_concurrent拼接而成的
    • 串行队列类型:OS_dispatch_queue_serial,验证如下
* 并发队列类型:`OS_dispatch_queue_concurrent`,验证如下![](https://img.haomeiwen.com/i2340353/ac444e89cd7608be.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 4.通过alloc+init初始化队列,即dq,其中在_dispatch_queue_init传参中根据dqai.dqai_concurrent的布尔值,就能判断队列是串行还是并发,而 vtable表示队列的类型,说明队列也是对象
    • 进入_dispatch_object_alloc -> _os_object_alloc_realized方法中设置了isa的指向,从这里可以验证队列也是对象的说法
* 进入`_dispatch_queue_init`方法,队列类型是`dispatch_queue_t`,并设置队列的相关属性![](https://img.haomeiwen.com/i2340353/bc9fbe87c7aea2e9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 5.通过_dispatch_trace_queue_create对创建的队列进行处理,其中_dispatch_trace_queue_create_dispatch_introspection_queue_create封装的宏定义,最后会返回处理过的_dq
* 进入`_dispatch_introspection_queue_create_hook -> dispatch_introspection_queue_get_info -> _dispatch_introspection_lane_get_info`中可以看出,与我们自定义的类还是有所区别的,`创建队列`在底层的实现是`通过模板创建`的![](https://img.haomeiwen.com/i2340353/8558202b2f11d069.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

②.3 总结

  • 队列创建方法dispatch_queue_create中的参数二(即队列类型),决定了下层中 max & 1(用于区分是 串行 还是 并发),其中1表示串行
  • queue 也是一个对象,也需要底层通过alloc + init 创建,并且在alloc中也有一个class,这个class是通过宏定义拼接而成,并且同时会指定isa的指向
  • 创建队列在底层的处理是通过模板创建的,其类型是dispatch_introspection_queue_s结构体

dispatch_queue_create底层分析流程如下图所示

③ 异步函数

③.1 dispatch_async

主要分析两个函数

  • _dispatch_continuation_init:任务包装函数
  • _dispatch_continuation_async:并发处理函数

③.2 _dispatch_continuation_init 任务包装器

主要是包装任务,并设置线程的回程函数,相当于初始化

主要有以下几步

  • 通过_dispatch_Block_copy拷贝任务
  • 通过_dispatch_Block_invoke封装任务,其中_dispatch_Block_invoke是个宏定义,根据以上分析得知是异步回调
  • 如果是同步的,则回调函数赋值为_dispatch_call_block_and_release
  • 通过_dispatch_continuation_init_f方法将回调函数赋值,即f就是func,将其保存在属性中

③.3 _dispatch_continuation_async 并发处理

这个函数中,主要是执行block回调

  • 其中的关键代码是dx_push(dqu._dq, dc, qos)dx_push是宏定义,如下所示
  • 而其中的dq_push需要根据队列的类型,执行不同的函数

在此我们通过符号断点调试执行函数

  • 运行demo,通过符号断点,来判断执行的是哪个函数,由于是并发队列,通过增加_dispatch_lane_concurrent_push符号断点,看看是否会走到这里
  • 运行发现,走的确实是_dispatch_lane_concurrent_push
  • 进入_dispatch_lane_concurrent_push源码,发现有两步,继续通过符号断点_dispatch_continuation_redirect_push_dispatch_lane_push调试,发现走的是_dispatch_continuation_redirect_push
  • 进入_dispatch_continuation_redirect_push源码,发现又走到了dx_push,即递归了,综合前面队列创建时可知,队列也是一个对象,有父类、根类,所以会递归执行到根类的方法
  • 接下来,通过根类的_dispatch_root_queue_push符号断点,来验证猜想是否正确,从运行结果看出,完全是正确的
  • 进入_dispatch_root_queue_push -> _dispatch_root_queue_push_inline ->_dispatch_root_queue_poke -> _dispatch_root_queue_poke_slow源码,经过符号断点验证,确实是走的这里,查看该方法的源码实现,主要有两步操作
    • 通过_dispatch_root_queues_init方法注册回调
    • 通过do-while循环创建线程,使用pthread_create方法

③.4 _dispatch_root_queues_init

  • 进入_dispatch_root_queues_init源码实现,发现是一个dispatch_once_f单例(请查看后续单例的底层分析们,这里不作说明),其中传入的func_dispatch_root_queues_init_once
  • 进入_dispatch_root_queues_init_once的源码,其内部不同事务的调用句柄都是_dispatch_worker_thread2

block回调执行的调用路径为:_dispatch_root_queues_init_once ->_dispatch_worker_thread2 -> _dispatch_root_queue_drain -> _dispatch_root_queue_drain -> _dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline -> _dispatch_client_callout -> dispatch_call_block_and_release

这个路径可以通过断点,bt打印堆栈信息得出

在这里需要说明一点的是,单例block回调和异步函数block回调是不同的

  • 单例中,block回调中的func_dispatch_Block_invoke(block)
  • 而异步函数中,block回调中的funcdispatch_call_block_and_release

④ 总结

综上所述,异步函数的底层分析如下

  • 准备工作: 首先,将异步任务拷贝并封装,并设置回调函数func
  • block回调:底层通过dx_push递归,会重定向到根队列,然后通过pthread_creat创建线程,最后通过dx_invoke执行block回调(注意dx_pushdx_invoke 是成对的)
    异步函数的底层分析流程如图所示

④ 同步函数

④.1 dispatch_sync

其底层的实现是通过栅栏函数实现的(栅栏函数的底层分析见后文)

④.2 _dispatch_sync_f

④.3 _dispatch_sync_f_inline

查看_dispatch_sync_f_inline源码,其中width = 1表示是串行队列,其中有两个重点:

  • 栅栏:_dispatch_barrier_sync_f(可以通过后文的栅栏函数底层分析解释),可以得出同步函数的底层实现其实是同步栅栏函数
  • 死锁:_dispatch_sync_f_slow,如果存在相互等待的情况,就会造成死锁

④.4 _dispatch_sync_f_slow 死锁

进入_dispatch_sync_f_slow,当前的主队列挂起、阻塞

  • 往一个队列中加入任务,会push加入主队列,进入_dispatch_trace_item_push
  • 进入__DISPATCH_WAIT_FOR_QUEUE__,判断dq是否为正在等待的队列,然后给出一个状态state,然后将dq的状态和当前任务依赖的队列进行匹配
  • 进入_dq_state_drain_locked_by -> _dispatch_lock_is_locked_by源码

如果当前等待的和正在执行的是同一个队列,即判断线程ID是否相等,如果相等,则会造成死锁

同步函数 + 并发队列 顺序执行的原因
_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline源码中,主要有三个步骤:

  • 将任务压入队列:_dispatch_thread_frame_push
  • 执行任务的block回调: _dispatch_client_callout
  • 将任务出队:_dispatch_thread_frame_pop

从实现中可以看出,是先将任务push队列中,然后执行block回调,在将任务pop,所以任务是顺序执行的

④.5 总结

同步函数的底层实现如下:

  • 同步函数的底层实现实际是同步栅栏函数
  • 同步函数中如果当前正在执行的队列和等待的是同一个队列,形成相互等待的局面,则会造成死锁
综上所述,同步函数的底层实现流程如图所示

⑤ dispatch_once 单例

在日常开发中,我们一般使用GCDdispatch_once来创建单例,如下所示

首先对于单例,我们需要了解两点

  • 执行一次的原因: 单例的流程只执行一次,底层是如何控制的,即为什么只能执行一次?
  • block调用时机: 单例的block是在什么时候进行调用的?

下面带着以上两点疑问,我们来针对单例的底层进行分析

⑤.1 dispatch_once

进入dispatch_once源码实现,底层是通过dispatch_once_f实现的

  • 参数1:onceToken,它是一个静态变量,由于不同位置定义的静态变量是不同的,所以静态变量具有唯一性
  • 参数2:block回调

⑤.2 dispatch_once_f

进入dispatch_once_f源码,其中的val是外界传入的onceToken静态变量,而func_dispatch_Block_invoke(block),其中单例的底层主要分为以下几步

  • val,也就是静态变量转换为dispatch_once_gate_t类型的变量l
  • 通过os_atomic_load获取此时的任务的标识符v
    • 如果v等于DLOCK_ONCE_DONE,表示任务已经执行过了,直接return
    • 如果 任务执行后,加锁失败了,则走到_dispatch_once_mark_done_if_quiesced函数,再次进行存储,将标识符置为DLOCK_ONCE_DONE
    • 反之,则通过_dispatch_once_gate_tryenter尝试进入任务,即解锁,然后执行_dispatch_once_callout执行block回调
  • 如果此时有任务正在执行,再次进来一个任务2,则通过_dispatch_once_wait函数让任务2进入无限次等待

⑤.3 _dispatch_once_gate_tryenter 解锁

查看其源码,主要是通过底层os_atomic_cmpxchg方法进行对比,如果比较没有问题,则进行加锁,即任务的标识符置为DLOCK_ONCE_UNLOCKED

⑤.4 _dispatch_once_callout 回调

进入_dispatch_once_callout源码,主要就两步

  • _dispatch_client_calloutblock回调执行
  • _dispatch_once_gate_broadcast:进行广播
  • 进入_dispatch_client_callout源码,主要就是执行block回调,其中的f等于_dispatch_Block_invoke(block),即异步回调
  • 进入 _dispatch_once_gate_broadcast -> _dispatch_once_mark_done源码,主要就是给dgo->dgo_once一个值,然后将任务的标识符为DLOCK_ONCE_DONE,即解锁

⑤.5 总结

针对单例的底层实现,主要说明如下:

  • 单例只执行一次的原理GCD单例中,有两个重要参数,onceTokenblock,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量ll主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return

  • block调用时机:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义.加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回

  • 多线程影响:如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的

单例的底层流程分析如下如所示


⑥ 栅栏函数

GCD中常用的栅栏函数,主要有两种

  • 同步栅栏函数dispatch_barrier_sync(在主线程中执行):前面的任务执行完毕才会来到这里,但是同步栅栏函数会堵塞线程,影响后面的任务执行
  • 异步栅栏函数dispatch_barrier_async:前面的任务执行完毕才会来到这里

栅栏函数最直接的作用就是 控制任务执行顺序,使同步执行

栅栏函数需要注意以下几点

  • 栅栏函数只能控制同一并发队列
  • 同步栅栏添加进入队列的时候,当前线程会被锁死,直到同步栅栏之前的任务和同步栅栏任务本身执行完毕时,当前线程才会打开然后继续执行下一句代码
  • 在使用栅栏函数时,使用自定义队列才有意义
    • 如果栅栏函数中使用全局队列运行会崩溃,原因是系统也在用全局并发队列,使用栅栏同时会拦截系统的,所以会崩溃
    • 如果将自定义并发队列改为串行队列,即serial ,串行队列本身就是有序同步 此时加栅栏,会浪费性能

⑥.1 异步栅栏函数

进入dispatch_barrier_async源码实现,其底层的实现与dispatch_async类似,这里就不再做分析了,有兴趣的可以自行探索下

⑥.2 同步栅栏函数

⑥.2.1 dispatch_barrier_sync

进入dispatch_barrier_sync源码,实现如下

⑥.2.2 _dispatch_barrier_sync_f_inline

进入_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline源码

主要有分为以下几部分

  • 通过_dispatch_tid_self获取线程ID
  • 通过_dispatch_queue_try_acquire_barrier_sync判断线程状态
* 进入`_dispatch_queue_try_acquire_barrier_sync_and_suspend`,在这里进行释放![](https://img.haomeiwen.com/i2340353/4ccaf0f24ed3b69a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  • 通过_dispatch_sync_recurse递归查找栅栏函数的target
  • 通过_dispatch_introspection_sync_begin对向前信息进行处理
  • 通过_dispatch_lane_barrier_sync_invoke_and_complete执行block并释放

⑦ 信号量

信号量的作用一般是用来使任务同步执行,类似于互斥锁,用户可以根据需要控制GCD最大并发数.前面我们已经说了怎么使用了

下面我们来分析其底层原理

⑦.1 dispatch_semaphore_create 创建

该函数的底层实现如下,主要是初始化信号量,并设置GCD的最大并发数,其最大并发数必须大于0

⑦.2 dispatch_semaphore_wait 加锁

该函数的源码实现如下,其主要作用是对信号量dsema通过os_atomic_dec2o进行了--操作,其内部是执行的C++atomic_fetch_sub_explicit方法

  • 如果value >= 0,表示操作无效,即执行成功
  • 如果value = LONG_MIN,系统会抛出一个crash
  • 如果value < 0,则进入长等待

其中os_atomic_dec2o的宏定义转换如下

os_atomic_inc2o(p, f, m) 
os_atomic_sub2o(p, f, 1, m)
_os_atomic_c11_op((p), (v), m, sub, -)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
        atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
        memory_order_##m); (__typeof__(_r))(_r op _v); })

将具体的值代入为

os_atomic_dec2o(dsema, dsema_value, acquire);
os_atomic_sub2o(dsema, dsema_value, 1, m)
os_atomic_sub(dsema->dsema_value, 1, m)
_os_atomic_c11_op(dsema->dsema_value, 1, m, sub, -)
_r = atomic_fetch_sub_explicit(dsema->dsema_value, 1),
等价于 dsema->dsema_value - 1

进入_dispatch_semaphore_wait_slow的源码实现,当value < 0时,根据等待事件timeout做出不同操作

⑦.3 dispatch_semaphore_signal 解锁

该函数的源码实现如下,其核心也是通过os_atomic_inc2o函数对value进行了++操作,os_atomic_inc2o内部是通过C++atomic_fetch_add_explicit

  • 如果value > 0,表示操作无效,即执行成功
  • 如果value < 0,则进入长等待

其中os_atomic_dec2o的宏定义转换如下

os_atomic_inc2o(p, f, m) 
os_atomic_add2o(p, f, 1, m)
os_atomic_add(&(p)->f, (v), m)
_os_atomic_c11_op((p), (v), m, add, +)
({ _os_atomic_basetypeof(p) _v = (v), _r = \
        atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p), _v, \
        memory_order_##m); (__typeof__(_r))(_r op _v); })

将具体的值代入为

os_atomic_inc2o(dsema, dsema_value, release);
os_atomic_add2o(dsema, dsema_value, 1, m) 
os_atomic_add(&(dsema)->dsema_value, (1), m)
_os_atomic_c11_op((dsema->dsema_value), (1), m, add, +)
_r = atomic_fetch_add_explicit(dsema->dsema_value, 1),
等价于 dsema->dsema_value + 1

⑦.4 总结

  • dispatch_semaphore_create 主要就是初始化限号量
  • dispatch_semaphore_wait是对信号量的value进行--,即加锁操作
  • dispatch_semaphore_signal 是对信号量的value进行++,即解锁操作
综上所述,信号量相关函数的底层操作如图所示

⑧ 调度组的原理

调度组的最直接作用是控制任务执行顺序,常见方式如下

⑧.1 dispatch_group_create

  • 进入dispatch_group_create源码
    主要是创建group,并设置属性,此时的groupvalue为0
  • 进入_dispatch_group_create_with_count源码,其中是对group对象属性赋值,并返回group对象,其中的n等于0

⑧.2 dispatch_group_enter 进组

进入dispatch_group_enter源码,通过os_atomic_sub_orig2odg->dg.bits--操作,对数值进行处理

⑧.3 dispatch_group_leave 出组

进入dispatch_group_leave源码,可知

  • -1 到 0,即++操作
  • 根据状态,do-while循环,唤醒执行block任务
  • 如果0 + 1 = 1enter-leave不平衡,即leave多次调用,会crash
  • 进入_dispatch_group_wake源码,do-while 循环进行异步命中,调用_dispatch_continuation_async执行
  • 进入_dispatch_continuation_async源码

这步与异步函数的block回调执行是一致的,这里不再作说明

⑧.4 dispatch_group_notify 通知

进入dispatch_group_notify源码,如果old_state等于0,就可以进行释放了

除了leave可以通过_dispatch_group_wake唤醒,其中dispatch_group_notify也是可以唤醒的

  • 其中os_mpsc_push_update_tail是宏定义,用于获取dg的状态码

⑧.5 dispatch_group_async

进入dispatch_group_async 源码,主要是包装任务异步处理任务

  • 进入_dispatch_continuation_group_async源码,主要是封装了dispatch_group_enter进组操作
  • 进入_dispatch_continuation_async源码,执行常规的异步函数底层操作.既然有了enter,肯定有leave,我们猜测block执行之后隐性的执行leave,通过断点调试,打印堆栈信息

  • 搜索_dispatch_client_callout的调用,在_dispatch_continuation_with_group_invoke

所以,完美的印证dispatch_group_async底层封装的是enter-leave

⑧.6 总结

  • enter-leave只要成对就可以,不管远近
  • dispatch_group_enter在底层是通过C++函数,对groupvalue进行--操作(即0 -> -1
  • dispatch_group_leave在底层是通过C++函数,对groupvalue进行++操作(即-1 -> 0
  • dispatch_group_notify在底层主要是判断groupstate是否等于0,当等于0时,就通知
  • block任务的唤醒,可以通过dispatch_group_leave,也可以通过dispatch_group_notify
  • dispatch_group_async 等同于enter - leave,其底层的实现就是enter-leave
综上所述,调度组的底层分析流程如下图所示

六、相关试题解析

① 异步函数+并行队列

下面代码的输出顺序是什么?


异步函数并不会阻塞主队列,会开辟新线程执行异步任务

分析思路如下图所示,红线表示任务的执行顺序
  • 主线程的任务队列为:任务1、异步block1、任务5,其中异步block1会比较耗费性能,任务1任务5的任务复杂度是相同的,所以任务1和任务5优先于异步block1执行
  • 异步block1中,任务队列为:任务2、异步block2、任务4,其中block2相对比较耗费性能,任务2任务4是复杂度一样,所以任务2和任务4优先于block2执行
  • 最后执行block2中的任务3
  • 在极端情况下,可能出现 任务2先于任务1任务5执行,原因是出现了当前主线程卡顿或者 延迟的情况

扩展一
并行队列 改成 串行队列,对结果没有任何影响,顺序仍然是1 5 2 4 3

扩展二
在任务5之前,休眠2s,即sleep(2),执行的顺序为:1 2 4 3 5,原因是因为I/O的打印,相比于休眠2s,复杂度更简单,所以异步block1 会先于任务5执行.当然如果主队列堵塞,会出现其他的执行顺序

② 异步函数嵌套同步函数 + 并发队列

下面代码的输出顺序是什么?


分析如下:

  • 任务1任务5的分析同前面一致,执行顺序为 任务1 任务5 异步block
  • 异步block中,首先执行任务2,然后走到同步block,由于同步函数会阻塞主线程,所以任务4需要等待任务3执行完成后,才能执行,所以异步block中的执行顺序是:任务2 任务3 任务4

③ 异步函数嵌套同步函数 + 串行队列(即同步队列)

下面代码的执行顺序是什么?会出现什么情况?为什么?


分析如下图所示,红色表示任务执行顺序,黑色虚线表示等待
  • 首先执行任务1,接下来是异步block,并不会阻塞主线程,相比任务5而言,复杂度更高,所以优先执行任务5,在执行异步block
  • 异步block中,先执行任务2,接下来是同步block同步函数会阻塞线程,所以执行任务4需要等待任务3执行完成,而任务3的执行,需要等待异步block执行完成,相当于任务3等待任务4完成
  • 所以就造成了任务4等待任务3任务3等待任务4,即互相等待的局面,就会造成死锁,这里有个重点是关键的堆栈 slow

扩展一
去掉任务4,执行顺序是什么?
还是会死锁,因为任务3等待的是异步block执行完毕,而异步block等待任务3.

④ 异步函数 + 同步函数 + 并发队列

下面代码的执行顺序是什么?(答案是 AC)
A: 1230789
B: 1237890
C: 3120798
D: 2137890


分析

  • 任务1任务2由于是异步函数+并发队列,会开启线程,所以没有固定顺序
  • 任务7任务8任务9同理,会开启线程,所以没有固定顺序
  • 任务3同步函数+并发队列,同步函数会阻塞主线程,但是也只会阻塞0,所以,可以确定的是 0一定在3之后在789之前

以下是不同的执行顺序的打印


⑤ 下面代码中,队列的类型有几种?

队列总共有两种: 并发队列串行队列

  • 串行队列:serialmainQueue
  • 并发队列:conqueglobalQueue

写在后面

和谐学习,不急不躁.我还是我,颜色不一样的烟火.

相关文章

网友评论

      本文标题:iOS之武功秘籍⑬: 多线程原理与GCD和NSOperation

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