跃迁引擎

空気を読んだ雨降らないでよ

iOS Research & Development


iOS多线程总结(GCD、NSOperation、NSTread)

对 iOS 多线程技术 GCD、NSOperation、NSTread的一些总结。

GCD替我们做了哪些工作(为什么要使用GCD)

基本概念

它是一个在线程池模式的基础上执行的并发任务。线程池的限制上线是64

为什么要用 GCD

  • GCD可用于多核并行运算
  • GCD会自动利用更多的CPU内核(比如双核、四核)
  • GCD会自动管理线程的生命周期创建线程、调度任务、销毁线程)
  • 工程师只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

核心概念

任务队列

  • 任务执行任务有两种方式:同步执行(sync)和异步执行(async)主要区别:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

    • 同步执行:等待/只能在当前线程中执行任务,不具备开启新线程的能力
    • 异步执行:不等待/可以在新的线程中执行任务,具备开启新线程的能力
  • 队列这里的队列指执行任务等待队列,即用来存放任务的队列采用FIFO(先进先出)的原则有两种队列:串行队列并发队列

    • 串行队列:只开启一个线程,一个任务执行完毕后,再执行下一个任务 SERIAL
    • 并发队列:可以开启多个线程,并且同时执行任务 CONCURRENT并发队列的并发功能只有在异步(dispatch_async)函数下才有效
  • 使用步骤使用步骤其实很简单,只有两步1.创建一个队列(串行队列或并发队列)2.将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)

    • 队列的创建方法/获取方法可以使用dispatch_queue_create来创建队列,需要传入两个参数
      • 第一个参数:队列的唯一标识符
      • 第二个参数:识别是串行队列还是并发队列
      • 串行队列:主队列(Main Dispatch Queue)主线程执行
      • 并发队列:GCD默认提供全局并发队列(Global Dispatch Queue)
        • 需要传入两个参数:1.表示队列优先级,一般默认 2.没什么用,一般用0
  • 任务的创建方法

    • 同步执行任务的创建方法:dispatch_sync

    • 异步执行任务创建方法:dispatch_async

    • 队列&任务组合方式

      • 1.同步执行 + 并发队列
      • 2.异步执行 + 并发队列
      • 3.同步执行 + 串行队列
      • 4.异步执行 + 串行队列
      • 5.同步执行 + 主队列
      • 6.异步执行 + 主队列
  • GCD 线程间的通信 其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作

  • GCD 的其他方法

    • 栅栏方法:dispatch_barrier_async 并发队列在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作
    • 延时执行方法:dispatch_after 主队列时间不精确
    • 一次性代码(只执行一次):dispatch_once保证线程安全且只执行一次
    • 快速迭代:dispatch_apply指定的次数for循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个元素
    • 队列组:dispatch_group 全局并发队列notify返回指定线程执行任务,一般是主线程
    • 信号量:dispatch_semaphore持有计数的信号,计数为0时等待,计数为1或大于1时,计数减1并执行
      • 保持线程同步,将异步执行任务转换为同步执行任务
      • 保证线程安全,为线程加锁

GCD在后台执行一个任务,小任务定时器,实现步骤:

  • 获取全局并发队列dispatch_get_global_queue
  • 将获取到的全局并发队列作为参数,创建队列dispatch_source_create
  • 设置定时器dispatch_source_set_timer
  • 设置响应分派源事件的block,在分派源指定的队列上运行dispatch_source_set_event_handler
  • 开始执行派发源dispatch_resume

实现一个GCD的线程池

一个简单线程池至少包含下列组成部分:

  • 线程池管理器(ThreadPoolManager):用于创建并管理线程池线程池管理器至少有下列功
    • 创建线程池
    • 销毁线程池
    • 添加新任务
  • 工作线程(WorkThread): 线程池中线程一个可以循环执行任务的线程,在没有任务时将等待
  • 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行规定了任务的入口,任务执行完后的收尾工作,任务的执行状态
  • 任务队列:用于存放没有处理的任务。提供一种缓冲机制优化:
    • 动态增加工作线程
    • 优化工作线程数目

写出GCD的执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
// 题目:写出NSLog的打印结果(来自美团 GCD 面试题)
__block int a = 0;
while (a < 5) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
a++;
});
}
NSLog(@"输出: %d", a);

// 答案:
// 输出结果为:a >= 5
// 原因:while循环内部执行并发耗时任务,ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,所以当执行a++时,Block外部也能访问到改变后的值;当a不满足循环条件而跳出时,并发任务可能仍在只执行,此时仍然会改变a的值,鉴于不同机器的CPU和线程差异影响,所以最终输出结果会大于等于5
// 注意:a的输出值结果是a>=5,但是a的实际结果会远远大于5(NSLog输出完成后,并发耗时任务可能尚未完全结束)

NSOpration 和 NSOperationQueue

简介

NSOperationNSOperationQueue是基于GCD的更高一层的封装,分别对应GCD的任务队列,完全地面向对象。

但是比GCD更简单易用、代码可读性也更高。NSOperation和NSOperationQueue对比GCD会带来一点额外的系统开销,但是可以在多个操作Operation中添加附属。

GCD与NSOpration的区别

  • 设置依赖关系
  • 设置监听进度
  • 设置优先级
  • 还能继承
  • 可以取消准备执行的任务
  • 比GCD会带来一点额外的系统开销
  • 比GCD更简单易用、代码可读性也更高

如何使用

可以通过start方法直接启动NSOperation子类对象,并且默认同步执行任务,将NSOperation子类对象添加到NSOperationQueue中,该队列默认并发的调度任务

开启操作的方式

开启操作有二种方式,一是通过start方法直接启动操作,该操作默认同步执行,二是将操作添加到NSOperationQueue中,然后由系统从队列中获取操作然后添加到一个新线程中执行,这些操作默认并发执行

具体实现

  • 方式一:直接由NSOperation子类对象启动。 首先将需要执行的操作封装到NSOperation子类对象中,然后该对象调用Start方法。
  • 方式二:当添加到NSOperationQueue对象中,由该队列对象启动操作。
    • 将需要执行的操作封装到NSOperation子类对象中
    • 将该对象添加到NSOperationQueue
    • 系统将NSOperation子类对象从NSOperationQueue中取出
    • 将取出的操作放到一个新线程中执行

使用队列来执行操作,分为2个阶段:第一阶段:添加到线程队列的过程,是上面的步骤1和2。第二阶段:系统自动从队列中取出线程,并且自动放到线程中执行,是上面的步骤3和4。

NSOperation

NSOperation是一个和任务相关的抽象类,不具备封装操作的能力,必须使用其子类

NSOperation⼦类的方式有3种:

  • 系统实现的具体子类:NSInvocationOperation
  • 系统实现的具体子类:NSBlockOperation
  • 自定义子类,实现内部相应的⽅法。该类是线程安全的,不必管理线程生命周期同步等问题。

NSInvocationOperation子类

NSInvocationOperation类是NSOperation的一个具体子类,管理作为调用指定的单个封装任务执行的操作。这个类实现了一个非并发操作。方法属性无论使用该子类的哪个在初始化的方法,都会在添加一个任务。 和NSBlockOperation子类不同的是,因为没有额外添加任务的方法使用NSInvocationOperation创建的对象只会有一个任务

创建操作对象的方式

  • 使用initWithTarget:selector:object:创建sel参数是一个或0个的操作对象
  • 使用initWithInvocation:方法,添加sel参数是0个或多个操作对象。

未添加到队列的情况下,创建操作对象的过程中不会开辟线程,会在当前线程中执行同步操作。创建完成后,直接调用start方法,会启动操作对象来执行,或者添加到NSOperationQueue队列中。

默认情况下,调用start方法不会开辟一个新线程去执行操作,而是在当前线程同步执行任务。只有将其放到一个NSOperationQueue中,才会异步执行操作。

NSBlockOperation子类

NSBlockOperation类是NSOperation的一个具体子类,它管理一个或多个块的并发执行。可以使用此对象一次执行多个块,而不必为每个块创建单独的操作对象。当执行多个块时,只有当所有块都完成执行时,才认为操作本身已经完成。

添加到操作中的块(block)将以默认优先级分配到适当的工作队列。

创建操作对象的方式

  • 可以通过blockOperationWithBlock:创建NSBlockOperation对象,在创建的时候也添加一个任务。如果想添加更多的任务,可以使用addExecutionBlock:方法。
  • 也可以通过init:创建NSBlockOperation对象。但是这种创建方式并不会在创建对象的时候添加任务,同样可以使用addExecutionBlock:方法添加任务。

对于启动操作和NSInvocationOperation类一样,都可以通过调用start方法和添加NSOperationQueue中来执行操作。

自定义子类

一般类NSInvocationOperation、NSBlockOperation就可以满足使用需要,当然还可以自己自定义子类。

创建的子类时,需要考虑到可能会添加到串行和并发队列的不同情况,需要重写不同的方法。对于串行操作,仅仅需要重新main方法就行,在这个方法中添加想要实现的功能。对于并发操作,重写四个方法:startasynchronousexecutingfinished。并且需要自己创建自动释放池,因为异步操作无法访问主线程的自动释放池。

注意:在自定义子类时,经常通过cancelled属性检查方法是否取消,并且对取消的做出响应。

线程安全

在NSOperation实例在多线程上执行是安全的,不需要添加额外的锁。

NSThread

NSThread是 Apple 官方提供面向对象操作线程的技术。有点事简单方便,可以直接操作线程对象,缺点是需要自己手动控制线程的生命周期

NSThread 能做的事情几乎都可以被 GCD 或 NSOperation 代替,使用场景较少,所以只做概念上的了解即可。

基本属性

线程字典

threadDictionary ,每个线程都维护了一个 key-value 的字典,它可以在线程里面的任何地方被访问。 你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。 比如,你可以使用它来存储在你的整个线程过程中 Run loop 里面多次迭代的状态信息。

线程优先级

threadPriority ,NSThread 可以手动设置线程的优先级。

线程名称

name ,NSThread 可以手动设置每个线程的名称。

栈大小

stackSize ,NSThread 可以手动设置线程使用栈区大小,默认是512K。

线程执行

NSThread 可以手动取消、结束正在执行的线程,以及判断线程的当前执行状态。

最近的文章

Dealloc的实现机制

今天来聊聊 Dealloc,它的实现机制是内存管理部分的重点,把这个知识点弄明白,对于全方位的理解 iOS 内存管理非常有帮助。本文将从源码的角度来解析 Dealloc 的实现机制。 …

, , 开始阅读
更早的文章

我是如何快学上手 Sketch 的

如何在短时间内快速学习并掌握 Sketch 常用的基础操作,本文记录了我从 0 基础开始的学习过程与快速上手的使用技巧总结,希望能够对你有所帮助。 …

, , 开始阅读
comments powered by Disqus