iOS 多线程记录(一)

前言

文章主要记录了iOS中多线程的基础概念及使用方法,在此作一个记录。一是加深印象,之后本身使用时也能够方便查找及复习,二是在本身的学习过程当中,总有大牛的文章做为引导,但愿本身也能给须要这方面知识的人一些帮助。git

关于这篇文章的Demo能够去个人github中MultiThreadDemo查看源码,若有不当之处,但愿你们指出。程序员

GCD方面的知识点,后续会继续更新。。。github

一、概述

1.1 准备知识

1.1.1 同步和异步

  • 同步: 必须等待当前语句执行完毕,才能够执行下一个语句。
  • 异步: 不用等待当前语句执行完毕,就能够执行下一个语句。

1.1.2 进程与线程

  • 进程
    • 概念:系统中正在运行的应用程序。
    • 特色:每一个进程都运行在其专用且受保护的内存空间,不一样的进程之间相互独立,互不干扰。
  • 线程
    • 概念:一个进程要想执行任务,必须得有线程 (每个进程至少要有一条线程) 线程是进程的基本执行单元,一个进程的全部任务都是在线程中执行的。
    • 特色:一条线程在执行任务的时候是串行(按顺序执行)的。若是要让一条线程执行多个任务,那么只能一个一个地按顺序执行这些任务。也就是说,在同一时间,一条线程只能执行一个任务

1.2 多线程基本概念及原理

  • 概念: 1个进程能够开启多条线程,多条线程能够并发(同时)执行不一样的任务。
  • 原理: 同一时间,CPU只能处理一条线程,即只有一条线程在工做多线程同时执行,实际上是CPU快速地在多条线程之间进行切换。若是CPU调度线程的速度足够快,就会形成多线程并发执行的”假象”。

1.3 优缺点

  • 优势api

    1. 能适当提升程序的执行效率。
    2. 能适当提升资源的利用率(CPU、内存利用率)
  • 缺点数组

    1. 开启线程须要占用必定的内存空间,若是开启大量的线程,会占用大量的内存空间,从而下降程序的性能。
    2. 线程越多,CPU在调度线程上的开销就越大。
    3. 线程越多,程序设计就会更复杂:好比 线程间通信、多线程的数据共享等。

1.4 总结

  1. 实际上,使用多线程,因为会开线程,必然就会消耗性能,可是却能够提升用户体验。因此,综合考虑,在保证良好的用户体验的前提下,能够适当地开线程。bash

  2. 在iOS中每一个进程启动后都会创建一个主线程(UI线程)。因为在iOS中除了主线程,其余子线程是独立于Cocoa Touch的,因此只有主线程能够更新UI界面。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。多线程

接下来就介绍一下iOS常见的几种多线程实现方式。并发

二、 三种多线程方案

2.1 Thread

2.1.1 介绍

  • 相对于GCD和Operation来讲是较轻量级的线程开发。
  • 使用比较简单,可是须要手动管理建立线程的生命周期、同步、异步、加锁等问题。

2.1.2 基本使用

这里介绍Thread的三种建立方式。下方三中建立方式中的Target类为:app

class Receiver: NSObject {
    @objc func runThread() {
        print(Thread.current)
    }
}
复制代码
  1. 建立实例,手动启动
// 1.建立线程
let thread_one = Thread(target: Receiver(), selector: #selector(Receiver.runThread), object: nil)

let thread_two = Thread {
    // TODO
}

// 2.启动线程
thread_one.start()

thread_two.start()
复制代码
  1. 类方法建立并启动
// 建立线程后自动启动线程
Thread.detachNewThread {
    // TODO
}

Thread.detachNewThreadSelector(#selector(Receiver.runThread), toTarget: Receiver(), with: nil)
复制代码
  1. 隐式建立并启动
let obj = Receiver()

// 隐式建立并启动线程
obj.performSelector(inBackground: #selector(obj.runThread), with: nil)
复制代码

2.1.3 线程间通讯

// 去主线程执行指定方法
performSelector(onMainThread: Selector, with: Any?, waitUntilDone: Bool, modes: [String]?)

// 去指定线程执行方法
perform(aSelector: Selector, on: Thread, with: Any?, waitUntilDone: Bool, modes: [String]?)
复制代码
  • Any?: 须要传递的数据
  • modes?: Runloop Mode值

2.1.4 线程优先级

设置线程优先级时,接收一个Double类型。异步

数值范围为:0.0 ~ 1.0。

对于新建立的thread来讲,Priority的值通常是 0.5。可是,由于优先级是由系统内核决定的,并不能保证这个值会是什么。

var threadPriority: Double { get set }
复制代码

2.1.5 线程状态与生命周期

与线程状态及生命周期相关的函数:

// - 启动线程的方法,进入就绪状态等待CPU调用
func start()

// - 阻塞(暂停)线程方法,进入阻塞状态
class func sleep(until date: Date)
class func sleep(forTimeInterval ti: TimeInterval)

// - 取消线程的操做,在线程执行完当前操做后,不会再继续执行任务
func cancel()

// - 强制中止线程,进入死亡状态
class func exit()
复制代码

cancel():方法并非当即取消当前线程,而是更改线程的状态,以指示它应该退出。

exit():应该避免调用此方法,由于它不会让线程有机会清理它在执行期间分配的任何资源。

  • 新建(New): 实例化线程对象
  • 就绪(Runnable): 向线程对象发送start消息,线程对象被加入可调度线程池等待CPU调度。
  • 运行(Running): CPU负责调度可调度线程池中线程的执行。线程执行完成以前,状态可能会在就绪和运行之间来回切换。就绪和运行之间的状态变化由CPU负责,程序员不能干预。
  • 阻塞(Blocked): 当知足某个预约条件时,可使用休眠或锁,阻塞线程执行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。
  • 死亡(Dead): 正常死亡,线程执行完毕。非正常死亡,当知足某个条件后,在线程内部停止执行/在主线程停止线程对象

状态转换图

系统还定义了几个NSNotification。若你对当前线程状态的改变感兴趣,能够订阅这几个通知:

// 当除了主线程外的最后一个线程退出时
static let NSDidBecomeSingleThreaded: NSNotification.Name

// 当线程接收到exit()消息时
static let NSThreadWillExit: NSNotification.Name

// 当建立第一个除主线程外的子线程时发布,然后再建立子线程时不会再发出通知。
// 通知的观察者的通知方法在主线程调用
NSWillBecomeMultiThreaded: NSNotification.Name
复制代码

2.1.6 其它经常使用方法

// 获取主线程
Thread.main
        
// 获取当前线程
Thread.current
        
// 获取当前线程状态
Thread.current.isCancelled
Thread.current.isFinished
Thread.current.isFinished
复制代码

2.2 Operation 和 OperationQueue

2.2.1 介绍

Operation是一个抽象类,能够用来封装一个任务,其中包含代码逻辑和数据。由于Operation是抽象类,因此编写代码时不能直接使用,要使用它的子类,系统默认提供的有NSInvocationOperation(Swift中不可用)和BlockOperation。

OperationQueue(操做队列)是用来控制一系列操做对象执行的。操做对象被添加进队列后,一直存在到操做被取消或者执行完成。队列里的操做对象执行的顺序由操做的优先级和操做之间的依赖决定。一个应用里能够建立多个队列进行操做处理。

优点
  1. 可添加完成的代码块,在操做完成后执行。
  2. 添加操做之间的依赖关系,方便的控制执行顺序。
  3. 设定操做执行的优先级。
  4. 能够很方便的取消一个操做的执行。
  5. 使用 KVO 观察对操做执行状态的更改:isExecuteing、isFinished、isCancelled。

2.2.2 基本使用

由Operation 和 OperationQueue的介绍能够获得使用步骤:

  1. 建立操做:先将须要执行的操做封装到一个 Operation 对象中。
  2. 建立队列:建立 OperationQueue 对象。
  3. 将操做加入到队列中:将 Operation 对象添加到 OperationQueue 对象中。

以后呢,系统就会自动将OperationQueue的Operation取出来,在新线程中执行操做。

①建立操做
  • NSInvocationOperation(Swift不支持)

默认是不会开启线程的,只会在当前的线程中执行操做,能够经过Operation和OperationQueue实现多线程。

// 1.建立 NSInvocationOperation 对象
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

// 2.调用 start 方法开始执行操做
// 不会开启线程
[op start];
复制代码
  • BlockOperation

BlockOperation 是否开启新线程,取决于操做的个数。若是添加的操做的个数多,就会自动开启新线程。固然开启的线程数是由系统来决定的。

// 1. 建立BlockOperation对象,并封装操做
let op = BlockOperation.init {
    print("init + \(Thread.current)")
}

// 2. 调用 start 方法开始执行操做
op.start()
复制代码
  • 自定义继承自 Operation 的子类

默认状况下,Operation的子类是同步执行的,若是要建立一个可以并发的子类,咱们可能须要重写一些方法。

  • start: 全部并行的 Operations 都必须重写这个方法,而后在你想要执行的线程中手动调用这个方法。注意:任什么时候候都不能调用父类的start方法。
  • main: 在start方法中调用,可是注意要定义独立的自动释放池与别的线程区分开。
  • isExecuting: 是否执行中,须要实现KVO通知机制。
  • isFinished: 是否已完成,须要实现KVO通知机制。
  • **isAsynchronous:**该方法默认返回false,表示非并发执行。并发执行须要自定义而且返回true。后面会根据这个返回值来决定是否并发。
复制代码
②建立队列

OperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本建立方法和特色。

// 主队列获取方法
let mainQueue = OperationQueue.main

// 自定义队列建立方法
let queue = OperationQueue()
复制代码
  • 主队列
    • 凡是添加到主队列中的操做,都会放到主线程中执行。
  • 自定义队列
    • 添加到这种队列中的操做,就会自动放到子线程中执行。
    • 同时包含了:串行、并发功能。
③将操做加入队列

Operation 须要配合 OperationQueue来实现多线程。咱们须要将建立好的操做加入到队列中去。有两种方法:

  1. addOperation(_ op: Operation)

将建立好的Operation或其子类的实例对象直接添加。

  1. addOperation(_ block: @escaping () -> Void)

直接经过block的方式添加一个操做至队列中。

2.2.3 串行,并行控制

OperationQueue 建立的自定义队列同时具备串行、并发功能。它的串行功能是经过属性 最大并发操做数—maxConcurrentOperationCount用来控制一个特定队列中能够有多少个操做同时参与并发执行。

注意:这里 maxConcurrentOperationCount控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操做数。并且一个操做也并不是只能在一个线程中运行。

  • 最大并发操做数:maxConcurrentOperationCount
    • maxConcurrentOperationCount 默认状况下为-1,表示不进行限制,可进行并发执行。
    • maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。
    • maxConcurrentOperationCount 大于1时,队列为并发队列。操做并发执行,固然这个值不该超过系统限制,即便本身设置一个很大的值,系统也会自动调整为 min{本身设定的值,系统设定的默认最大值}。
let queue = OperationQueue()

queue.maxConcurrentOperationCount = 1

queue.addOperation {
    sleep(1)
    print("1---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
    sleep(1)
    print("2---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
    sleep(1)
    print("3---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}
queue.addOperation {
    sleep(1)
    print("4---\(Thread.current)----\(Date.timeIntervalSinceReferenceDate)")
}

-----最大并发操做数为1,输出结果:------
1---<NSThread: 0x600001ddc200>{number = 5, name = (null)}----576945144.766482
2---<NSThread: 0x600001dd0280>{number = 6, name = (null)}----576945145.775298
3---<NSThread: 0x600001dfbd00>{number = 4, name = (null)}----576945146.775842
4---<NSThread: 0x600001dd0280>{number = 6, name = (null)}----576945147.779273

-----最大并发操做数为3,输出结果:------
2---<NSThread: 0x6000018dc0c0>{number = 5, name = (null)}----576945253.401897
1---<NSThread: 0x6000018c5d00>{number = 7, name = (null)}----576945253.401891
3---<NSThread: 0x6000018ca540>{number = 6, name = (null)}----576945253.401913
4---<NSThread: 0x6000018dc100>{number = 8, name = (null)}----576945254.403032
复制代码

上方输出的结果中,分析线程及输出时间能够看出:从当最大并发操做数为1时,操做是按顺序串行执行的。当最大操做并发数为3时,有3个操做是并发执行的,延迟1s后执行另外一个。而开启线程数量是由系统决定的,不须要咱们来管理。

2.2.4 操做依赖

Operation 提供了3个接口供咱们管理和查看依赖。

// 添加依赖,使当前操做依赖于操做 op 的完成。
func addDependency(_ op: Operation)

// 移除依赖,取消当前操做对操做 op 的依赖。
func removeDependency(_ op: Operation)

// 必须在当前对象开始执行以前完成执行的操做对象数组。
var dependencies: [Operation] { get }
复制代码

经过添加操做依赖,不管运行几回,其结果都是 op2 先执行,op1 后执行。

let queue = OperationQueue()

let op1 = BlockOperation {
    print("op1")
}
let op2 = BlockOperation {
    print("op2")
}

op1.addDependency(op2)

queue.addOperation(op1)
queue.addOperation(op2)

----输出结果:----
op2
op1
复制代码

2.2.5 线程优先级

OperationQueue 提供了queuePriority(优先级)属性,queuePriority属性适用于同一操做队列中的操做,不适用于不一样操做队列中的操做。默认状况下,全部新建立的操做对象优先级都是normal。可是咱们能够经过赋值来改变当前操做在同一队列中的执行优先级。

// 优先级的取值
public enum QueuePriority : Int {
        case veryLow
        case low
        case normal // default value
        case high
        case veryHigh
    }
复制代码

对于添加到队列中的操做,首先进入准备就绪的状态(就绪状态取决于操做之间的依赖关系),而后进入就绪状态的操做的开始执行顺序(非结束执行顺序)由操做之间相对的优先级决定(优先级是操做对象自身的属性)。

理解了进入就绪状态的操做,那么咱们就理解了queuePriority 属性的做用对象。

  • queuePriority 属性决定了进入准备就绪状态下的操做之间的开始执行顺序。而且,优先级不能取代依赖关系。
  • 若是一个队列中既包含高优先级操做,又包含低优先级操做,而且两个操做都已经准备就绪,那么队列先执行高优先级操做。
  • 若是,一个队列中既包含了准备就绪状态的操做,又包含了未准备就绪的操做,未准备就绪的操做优先级比准备就绪的操做优先级高。那么,虽然准备就绪的操做优先级低,也会优先执行。优先级不能取代依赖关系。若是要控制操做间的启动顺序,则必须使用依赖关系。

2.2.6 线程间通讯

let queue = OperationQueue()

let op = BlockOperation {
    print("异步操做 -- \(Thread.current)")
    
    // 回到主线程
    OperationQueue.main.addOperation({
        print("回到主线程了 -- \(Thread.current)")
    })
}

queue.addOperation(op)

-----输出结果:-----
异步操做 -- <NSThread: 0x60000102f540>{number = 3, name = (null)}
回到主线程了 -- <NSThread: 0x60000100d680>{number = 1, name = main}

复制代码

2.2.7 其它经常使用方法

  • Operation 经常使用属性和方法
1. 取消操做的方法
	* func cancel() 可取消操做,实质是标记 isCancelled 状态。
2. 判断操做状态的方法
	* isFinished 判断操做是否已经结束。
	* isCancelled 判断操做是否已经标记为取消。
	* isExecuting 判断操做是否正在在运行。
	* isAsynchronous 判断操做是否异步执行其任务。
	* isReady 判断操做是否处于准备就绪状态,这个值和操做的依赖关系相关。
3. 操做同步
	* func waitUntilFinished() 阻塞当前线程,直到该操做结束。可用于线程执行顺序的同步。
	* completionBlock: (() -> Void)? 会在当前操做执行完毕时执行 completionBlock。
复制代码
  • OperationQueue 经常使用属性及方法
1. 取消/暂停/恢复操做
	* func cancelAllOperations() 能够取消队列的全部操做。
	* isSuspended 判断及设置队列是否处于暂停状态。true为暂停状态,false为恢复状态。
2. 操做同步
	* func waitUntilAllOperationsAreFinished() 阻塞当前线程,直到队列中的操做所有执行完毕。
3. 添加/获取操做
	* func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) 向队列中添加操做数组,wait 标志是否阻塞当前线程直到全部操做结束
	* operations 当前在队列中的操做数组(某个操做执行结束后会自动从这个数组清除)。
	* operationCount 当前队列中的操做数。
4. 获取队列
	* current 获取当前队列,若是当前线程不是在 OperationQueue 上运行则返回 nil。
	* main 获取主队列。
复制代码

注意:

  1. 这里的暂停和取消(包括操做的取消和队列的取消)并不表明能够将当前的操做当即取消,而是当当前的操做执行完毕以后再也不执行新的操做。
  2. 暂停和取消的区别就在于:暂停操做以后还能够恢复操做,继续向下执行;而取消操做以后,全部的操做就清空了,没法再接着执行剩下的操做。