日志7. 多线程 GCD和NSOperation

632 阅读30分钟
  • ① 线程、进程与队列

    ①.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取代,以下是相关的枚举值

5 iOS中多线程的实现方案

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

6 线程安全问题

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

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

互斥锁

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

自旋锁

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

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

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

  • 不同点:

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

    • 当前的任务状态比较短小精悍时,用自旋锁

    • 反之的,用互斥锁

6.2 atomic与nonatomic 的区别

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

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

iOS 开发的建议

  • 所有属性都声明为 nonatomic

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

六、相关试题解析

① 异步函数+并行队列

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

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

分析思路如下图所示,红线表示任务的执行顺序

  • 主线程的任务队列为:任务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

7 线程间通讯

  • 直接消息传递: 通过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是苹果官方提供面向对象的线程操作技术,是对thread的上层封装,比较偏向于底层.简单方便,可以直接操作线程对象,使用频率较少.

① 创建线程

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

  • 通过init初始化方式创建

  • 通过detachNewThreadSelector构造器方式创建

  • 通过performSelector...方法创建,主要是用于获取主线程,以及后台线程

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

类方法

常用的类方法有以下几个

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

① GCD简介

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

GCD的优势:

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

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

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

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

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

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

  • 任务使用block封装

  • 任务的block没有参数也没有返回值

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

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

两种执行方式的主要区别有两点:

  • 是否等待队列的任务执行完毕

  • 是否具备开启新线程的能力

队列

多线程中所说的队列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实现多线程 ![](data:image/svg+xml;utf8,)

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

![](data:image/svg+xml;utf8,)

④ NSOperationQueue

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

  • 主队列:主队列上的任务是在主线程执行的
  • 其他队列(非主队列):加入到非主队列中的任务默认就是并发,开启多线程

例如我们在 ② NSBlockOperation 中说的那样.

⑤ 执行顺序

下列代码可以证明操作与队列的执行效果是异步并发的 ![](data:image/svg+xml;utf8,)

⑥ 设置优先级

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

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

⑦ 设置并发数

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

⑧ 添加依赖

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

⑨ 线程间通讯

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

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

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

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

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

五、GCD底层分析

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

  • 队列创建

  • 异步函数

  • 同步函数

  • 单例的原理

  • 栅栏函数的原理

  • 信号量的原理

  • 调度组的原理

作者:長茳
链接:juejin.cn/post/693719…
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。