了解DispatchQueues

最初发布于 swiftrocks.com

这些“ DispatchQueues”到底是什么?
为什么必须使用它将UI代码发送到主线程? 如果我什么也不做,它显然仍然有效。
这些“服务质量”队列有什么意义? 我将.main用于所有内容,并且从未遇到问题。
如果调用DispatchQueue.main.sync,为什么会崩溃? 有什么意义呢?
到底这是什么主线程?

如果您要开发iOS应用程序超过几个星期,那么您之前可能已经处理过并发代码。 如果您没有操作系统的知识,那么您可能已经问过自己其中一个问题。

一般而言,多线程是很难完全理解的事情,但是了解CPU如何处理并发性是编写可以完成您期望的工作的优质快速代码的关键。 否则,您可能正在滥用用户的CPU,但认为一切都很好,因为它们太快了,您无法注意到问题所在。

在我们能够回答这些问题之前,我们需要退后一步,了解事情在幕后的运作方式。

流程的定义非常简单:它是一个正在运行的程序。 您的应用是一个过程,Slack是一个过程,Safari是一个过程,依此类推。 它包含指令列表(您的汇编格式代码),并位于磁盘上,直到用户希望运行它为止。 然后,操作系统将把该进程加载到内存中,启动一个指令指针 ,告诉我们当前正在执行该程序的哪条指令,然后让CPU顺序执行其指令,直到结束为止,从而终止该进程。

 单线程进程的地址空间 
|-------------------------|-
| 说明|
|--------------------------------
| 全球数据
|--------------------------------
| 分配的数据(引用类型)|
|--------------------------------
| 什么都没有(堆栈和malloc的数据向此处增长)|
|--------------------------------
| 堆栈(值类型(如果可能),参数,返回)|
|--------------------------------

每个进程都有自己专用的物理内存部分。 他们不与其他进程共享这些地址。

不行 您正在经历的是一种幻想,它是由CPU所具有的荒谬速度造成的。

一个CPU不能同时做两件事。 对于具有多个内核的CPU,情况略有不同,但是为简单起见,我们假设只有一个CPU:发生的事情是它在Safari中执行某项,然后在Spotify中执行某项,然后在iOS中执行某项,然后在Safari中再次执行某项,依此类推上。 操作系统将把CPU为特定进程所做的任何事情保存在内存中(以寄存器和指针的形式),决定下一个要运行的进程,检索该进程正在做什么,让CPU运行该进程。一会儿,然后重复。 这称为上下文切换 ,它非常非常快地发生,给人的印象是它实际上可以一次运行多个操作。 (在具有多个内核的CPU中,可以将工作划分到各个内核之间,实际上一次执行多个操作。但是,在使用所有内核时,同样的原理也适用。)

操作系统决定下一步应该运行什么的确切方法相当复杂(如果您有兴趣,请阅读文章结尾的书),但是您应该知道,可以手动决定什么是“优先级” ”在我们的应用中。 (iOS的“服务质量”现在是否开始有意义?)

多线程程序具有一个以上的执行点,而不是从main()函数开始并在下面几行结束的某个exit()处结束的单线程进程的经典概念,执行)。 也许另一种思考方式是,每个线程都非常像一个单独的进程,只是有一个区别:它们共享相同的地址空间,因此可以访问相同的数据。

 多线程进程的地址空间 
|-------------------------|-
| 说明|
|--------------------------------
| 全球数据
|--------------------------------
| 分配的数据(引用类型)|
|--------------------------------
| 什么都没有(堆栈和malloc的数据向此处增长)|
|--------------------------------
| 线程2的堆栈|
|--------------------------------
| 线程1的堆栈|
|--------------------------------

就像进程一样,CPU无法同时运行两个线程-它们像进程一样被上下文切换作为目标。 CPU在Safari的线程1(正在进行一些UI更新)中运行某些内容,然后在Spotify的线程3(正在下载歌曲)中运行某些内容,然后在Safari的线程2(正在对DNS执行ping操作)中运行某些内容,依此类推。

您的iOS应用有多个线程。 主线程只是应用程序执行的初始起点(从didFinishLaunchingWithOptions )。 Main Thread每帧执行一次循环( RunLoop ),该循环在需要时绘制当前屏幕,处理UI事件(如触摸)并执行DispatchQueue.main的内容。 它会一直执行直到应用终止。 它具有极高的优先级–几乎所有内容都可以立即执行。 这就是为什么需要将UI代码路由到主线程–通过在其外部执行一些可更改UI的代码,您的代码可能开始正常运行,只是突然间切换上下文 ,这是因为更重要的内容已到达操作系统(例如通知)。 然后,您的UI更新将被延迟,从而给您的用户带来糟糕的体验。

但是,您不能简单地在主线程上执行所有操作。 由于此线程处理与屏幕绘制/ UI更新有关的所有内容,因此如果您在其上执行巨大任务,它将无法执行任何其他操作,直到结束。 这就是为什么我们需要几个线程(执行点)开始的原因。

 @IBAction func actionOne(_ sender: Any) { 
//Button actions are in the Main Thread.
//This takes about 5 seconds to finish
var counter = 0
for _ in 0..<1000000000 {
counter += 1
//The screen is totally frozen here.
//How can I scroll my screen (an UI action)
//If I blocked the thread by doing this meaningless thing?
//The scroll action is waiting to be run
//but it can't because it's also a Main Thread action.
//You can't simply context switch actions
//on the same thread.
//This needs to be run in a different thread.
}
}

后台线程是不是主线程的任何东西。 它们可以与主线程一起运行(就像它们是一个不同的过程,但是要记住线程的定义!),从而在不干扰主线程的UI更新的情况下处理复杂的任务。 在iOS中,产生后台线程的最安全方法是使用DispatchQueues 。 但是,请注意DispatchQueues不是线程–它们只是闭包队列, 最终将被转发到相关线程。 DispatchQueue将在需要时自动创建并重用线程池,从您那里抽象出手动生成线程的麻烦并处理这样做的潜在问题。

主线程将按顺序运行DispatchQueue.main的内容(即,动作2仅在动作1结束后发生),而DispatchQueue.global(qos:)的内容将同时 (一切都同时)运行到后台线程中(s)(如果有多个操作),其优先级等于所选QoS的优先级。 如果您想要自定义行为(例如将闭包转发到后台线程,但以串行方式转发的队列),则可以创建自己的DispatchQueue

通过为一项操作分配服务质量,您可以表明其重要性,然后系统会对其进行优先级排序并相应地进行计划。

由于高优先级的工作比低优先级的工作执行得更快,资源更多,因此与低优先级的工作相比,通常需要更多的精力。 为您的应用执行的工作准确地指定适当的QoS类可确保您的应用具有响应能力和能源效率。

对于几种不同类型的操作,后台线程的QoS有几个级别,但是没有一个比主线程的优先级更高的QoS(毕竟,如果后台任务阻止了UI更新,那就没有意义了,不是吗?认为?)。 服务质量是:

UserInteractive :用于必须立即处理的工作。
UserInitiated :用于几乎瞬时的工作,例如几秒钟或更短的时间。
Utility :用于可能需要一些时间的工作,例如API调用。
背景 :需要很长时间的工作。

通过使用Instruments ,我们可以看到不同的QoS级别如何影响代码的执行。

 @IBAction func actionOne(_ sender: Any) { 
//We already are in the main thread, but we will use a dispatch operation
//to see how long it takes for the task to begin.
DispatchQueue.main.async { [unowned self] in
self.timeIntensiveTask()
}
}

在按下IBAction之后,该任务立即执行,并花费了大约5秒钟来完成。 但是,由于我们阻塞了线程,整个屏幕都被冻结了。

 @IBAction func actionOne(_ sender: Any) { 
DispatchQueue.main.global(qos: .userInitiated).async { [unowned self] in
self.timeIntensiveTask()
}
}

产生了一个新线程,当我按下IBAction之后,任务几乎立即执行,大约需要5秒钟才能完成。 这次没有屏幕冻结! 该线程是完全独立的。

 @IBAction func actionOne(_ sender: Any) { 
DispatchQueue.main.global(qos: .background).async { [unowned self] in
self.timeIntensiveTask()
}
}

就像UserInitiated一样,产生了一个线程,但是在这种情况下,不仅开始任务花费了一些时间-而且结束也花了将近10秒! 该较低优先级的线程延迟并减少了对系统资源的访问。 但是,这很好:如果您要将任务发送到后台QoS队列,则意味着您不想通过集中精力破坏用户的CPU。

 @IBAction func actionOne(_ sender: Any) { 
DispatchQueue.main.async { [unowned self] in
self.timeIntensiveTask()
}
DispatchQueue.main.async { [unowned self] in
self.timeIntensiveTask()
}
DispatchQueue.main.async { [unowned self] in
self.timeIntensiveTask()
}
}
 @IBAction func actionOne(_ sender: Any) { 
DispatchQueue.main.global(qos: .background).async { [unowned self] in
self.timeIntensiveTask()
}
DispatchQueue.main.global(qos: .background).async { [unowned self] in
self.timeIntensiveTask()
}
DispatchQueue.main.global(qos: .background).async { [unowned self] in
self.timeIntensiveTask()
}
}

如果多线程进程的概念令人.async.sync我们需要注意.async.sync操作的定义。

一个常见的误解是认为DispatchQueue.async意味着在后台执行某些操作,而事实并非如此。

actionOne()的输出是什么?

 @IBAction func actionOne(_ sender: Any) { 
DispatchQueue.main.async { [unowned self] in
print("async started")
self.timeIntensiveTask()
print("async ended")
}
print("sync task started")
timeIntensiveTask()
print("sync task ended")
}

private func timeIntensiveTask() {
var counter = 0
for _ in 0..<1000000000 {
counter += 1
}
}

答案将始终是:

 sync task started 
sync task ended
async task started
async task ended

如果您认为这两个任务将同时开始,请考虑一下此方法的上下文:我们正在将任务分派到主线程,但是actionOne已经在主线程上了! 线程无法同时运行两个指令序列,这就是我们拥有不同线程的原因。

async任务也只会在sync任务之后执行(并且永远不会执行),因为DispatchQueue.main任务只会在主线程的RunLoop的末尾开始执行,而RunLoop被我们的同步任务阻止了。 如果actionOne恰好位于不同的线程中,或者async任务恰好位于不同的DispatchQueue ,则任务将按照与DispatchQueue async任务的速度有关的顺序一起启动。

DispatchQueue.async意思是: 确保最终在线程X(取决于您使用的队列是主线程还是任何其他全局后台线程)上执行此任务,但是我不在乎细节。 我会继续做我的事情。

相反, DispatchQueue.sync意思是: 确保此任务最终在线程X上执行。执行此操作时请警告我,因为在此任务完成运行之前,我还将阻止自己(调用线程)。

鉴于此,您认为以下actionOne()的输出是什么?

 @IBAction func actionOne(_ sender: Any) { 
DispatchQueue.main.sync { [unowned self] in
print("a")
}
print("b")
}

sync任务将转发到队列,并且主线程将冻结,直到打印"a"为止。 任务被发送到主线程,该线程被冻结,因为它正在等待任务运行。 但是任务无法运行,因为线程被冻结,等待任务运行,并且不断地运行直到您的应用决定崩溃为止。 您不能从线程本身调用sync调度,它必须来自其他地方。 什么都不会在这里打印。 如您所知,这称为死锁

希望这是回答本文开头的问题。 并发是一个非常广泛的问题,在iOS中, DispatchQueues只是解决并发问题的一种方法。 AtomicityOperationQueuesLocksSemaphores的形状还有更多。 但是, DispatchQueues是iOS中最常用的并发工具,并且当被理解时,它是编写有效的多线程代码的关键之一。

操作系统:三个简单的部分
Apple Docs:QoS
Apple Docs:线程管理
Swift并行编程基础


最初发布于 swiftrocks.com