阅读 339

iOS 多线程记录(一)

前言

文章主要记录了iOS中多线程的基础概念及使用方法,在此做一个记录。一是加深印象,以后自己使用时也可以方便查找及复习,二是在自己的学习过程中,总有大牛的文章作为引导,希望自己也能给需要这方面知识的人一些帮助。

关于这篇文章的Demo可以去我的github中MultiThreadDemo查看源码,如有不当之处,希望大家指出。

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

1、概述

1.1 准备知识

1.1.1 同步和异步

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

1.1.2 进程与线程

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

1.2 多线程基本概念及原理

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

1.3 优缺点

  • 优点

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

    1. 开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,从而降低程序的性能。
    2. 线程越多,CPU在调度线程上的开销就越大。
    3. 线程越多,程序设计就会更复杂:比如 线程间通讯、多线程的数据共享等。

1.4 总结

  1. 实际上,使用多线程,由于会开线程,必然就会消耗性能,但是却可以提高用户体验。所以,综合考虑,在保证良好的用户体验的前提下,可以适当地开线程。

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

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

2、 三种多线程方案

2.1 Thread

2.1.1 介绍

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

2.1.2 基本使用

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

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. 暂停和取消的区别就在于:暂停操作之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,无法再接着执行剩下的操作。
关注下面的标签,发现更多相似文章
评论