原文:https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
Grand Central Dispatch(GCD)是用于管理并发操作的低级API。GCD可以通过将计算量大的任务推迟到后台来帮助您提高应用程序的响应速度。与锁和线程相比,这是一个更简单的并发模型。
在Swift 3中,GCD得到了重大改进,从基于C的API转变为“Swiftier”API,包含新类和新的数据结构。
在这个由两部分组成的Grand Central Dispatch教程中,您将学习GCD的细节。第一部分将解释GCD的功能,并展示几个基本的GCD功能。在第二部分中,您将学习GCD提供的一些高级功能。
您将建立一个名为GooglyPuff的现有应用程序上。GooglyPuff是一款非优化的,“线程不安全”的应用程序,它使用Core Image的人脸检测API在检测到的人脸眼睛上覆盖Googly。您可以选择图像从照片库中应用此效果,或选择从互联网上下载的图像。
如果您选择接受它,您在本教程中的任务是使用GCD优化应用程序,并确保您可以安全地从不同线程调用代码。
入门
下载本教程的入门项目并解压缩。在Xcode中运行项目以查看您必须使用的项目。
主屏幕最初是空的。点击+然后选择Le Internet从互联网下载预定义的图像。点击第一张图片,你会看到添加在脸上的有趣的眼睛。
本教程中将主要介绍四个类:
- PhotoCollectionViewController:初始视图控制器。它将所选照片显示为缩略图。
- PhotoDetailViewController:从PhotoCollectionViewController图像中显示选定的照片并添加有趣的眼睛。
- Photo:这是描述照片属性的协议。它提供了图像,缩略图及其相应的状态。提供了两个实现该协议的类:DownloadPhoto它从一个实例实例化一张照片URL,并AssetPhoto从一个实例实例化一张照片PHAsset。
- PhotoManager:这管理所有Photo对象。
该应用有几个问题。运行应用程序时可能会注意到的一点是,下载完成警告为时过早。您将在本系列的第二部分中解决这个问题。
在第一部分中,您将进行一些改进,包括优化googly-fying过程并使PhotoManager线程安全。
GCD概念
要理解GCD,你需要熟悉与并发和线程相关的几个概念。
并发
在iOS中,一个进程或应用程序由一个或多个线程组成。线程由操作系统调度程序独立管理。每个线程可以同时执行,但是由系统决定是否发生这种情况以及它是如何发生的。
单核设备可以通过时间片实现并发。他们将运行一个线程,执行上下文切换,然后运行另一个线程。
另一方面,多核设备通过并行执行同时执行多个线程。
GCD建立在线程之上。它下面管理一个共享线程池。使用GCD可以添加代码块或工作项来分派队列,GCD决定在哪个线程上执行它们。
在您构建代码时,您会发现可以同时运行的代码块,有些代码块不应该运行。这就允许你使用GCD来利用并发执行。
请注意,GCD根据系统和可用系统资源决定需要多少并行性。重要的是要注意并行性需要并发性,但并发并不能保证并行性。
基本上,并发是关于结构,而并行则是关于执行。
队列
GCD提供调度队列DispatchQueue
来管理您提交的任务,并以FIFO的顺序执行它们,以确保提交的第一项任务是第一项任务的启动。
调度队列是线程安全的,这意味着您可以同时从多个线程访问它们。当您了解调度队列如何为自己的代码的某些部分提供线程安全性时,GCD的好处就显而易见了。这里的关键是要选择合适的种类调度队列和对的调度功能提交您的代码到队列中。
队列可以是串行或并发的。串行队列保证在任何给定时间只有一个任务运行。GCD控制执行时间。您不会知道一个任务结束和下一个任务开始之间的时间长短:
并发队列允许多个任务同时运行。保证任务按照他们添加的顺序开始。任务可以按任意顺序完成,并且您不知道下次启动任务所需的时间,也不知道在任何给定时间运行的任务数量。
请参阅下面的示例任务执行:
请注意,任务1,任务2和任务3是如何快速开始的。另一方面,任务1花了一些时间在任务0之后开始。另外注意,当任务3在任务2之后开始时,它首先完成。
何时开始任务的决定完全取决于GCD。如果一个任务的执行时间与另一个任务的执行时间重叠,则由GCD决定是否应该在不同的核心上运行(如果有),或者执行上下文切换以运行不同的任务。
GCD提供三种主要类型的队列:
- 主队列:在主线程上运行并且是一个串行队列。
- 全局队列:由整个系统共享的并发队列。有四个这样的队列具有不同的优先级:高,默认,低和后台。后台优先级队列被I / O限制。
- 自定义队列:您创建的可以是串行或并发队列。实际上这些都是由全局队列处理的。
设置全局并发队列时,不要直接指定优先级。而是指定一个服务质量(QoS)类属性。这将表明任务的重要性,并指导GCD确定优先任务。
QoS类是:
- User-interactive:这表示需要立即完成的任务才能提供良好的用户体验。用于UI更新,事件处理和需要低延迟的小型工作负载。在执行你的应用程序期间,在这个类中完成的工作总量应该很小。这应该在主线程上运行。
- User-initiated:代表从UI启动的任务,可以异步执行。应在用户等待立即结束时使用,以及用于继续用户交互所需的任务。这将被映射到高优先级的全局队列中。
- Utility:这表示长时间运行的任务,通常具有用户可见的进度指示器。将其用于计算,I / O,网络连接,连续数据类似任务。本课程旨在提高能源效率。这将被映射到低优先级的全局队列中。
- Background:这表示用户没有直接意识到的任务。将其用于预取,维护和其他不需要用户交互且时间不敏感的任务。这将被映射到后台优先级全局队列中。
同步与异步
使用GCD,您可以同步或异步分派任务。
一个同步功能将控制权返回给调用者的任务完成之后。
异步函数立即返回, 命令任务完成, 但不等待它。因此, 异步函数不会阻止当前执行线程继续运行到下一个函数
管理任务
到目前为止,你已经听说过很多任务。为了本教程的目的,您可以将任务视为闭包。闭包是可以存储和传递自包含的,可调用的代码块。
您提交的任务给DispatchQueue
由封装DispatchWorkItem
。您可以配置DispatchWorkItem
诸如其QoS等级的行为或是否产生新的分离线程。
处理后台任务
所有这些压抑的GCD知识,现在是您第一次改进应用程序的时候了!
回到应用程序并添加照片库中的一些照片或使用Le Internet选项下载一些照片。点按照片。注意照片细节视图显示需要多长时间。在较慢的设备上查看较大图像时,延迟更加明显。
重载视图控制器viewDidLoad()
很容易造成视图出现之前的长时间等待。如果它在加载时不是绝对必要的,最好将工作卸载到后台。
这听起来像一个工作DispatchQueue
的async
!
打开PhotoDetailViewController.swift。修改viewDidLoad()
并替换这两行:
let overlayImage = faceOverlayImageFromImage(image)
fadeInNewImage(overlayImage)
With the following code:
DispatchQueue.global(qos: .userInitiated).async { // 1
let overlayImage = self.faceOverlayImageFromImage(self.image)
DispatchQueue.main.async { // 2
self.fadeInNewImage(overlayImage) // 3
}
}
以下是代码的一步一步做的事情:
- 将工作移至后台全局队列,并异步运行闭包中的工作。这可以让
viewDidLoad()
主线程更早完成,并使装载感觉更加活泼。同时,人脸检测处理开始并在稍后的时间结束。 - 此时,人脸检测处理已完成,并且您已生成新图像。既然你想使用这个新的图像来更新你的
UIImageView
,你可以在主队列中添加一个新的闭包。请记住 - 您必须始终访问UIKit
主线程上的课程! - 最后,您更新
fadeInNewImage(_:)
用于执行新的googly眼睛图像的淡入转换的UI 。
构建并运行应用程序。通过Le Internet选项下载照片。选择一张照片,你会注意到视图控制器加载速度明显加快,并在短暂延迟后添加了googly眼睛:
这增加了应用程序的前后效果,因为添加了googly眼睛。即使你试图加载一个非常庞大的图像,你的应用程序也不会像加载视图控制器时那样挂起。
一般来说,async
当您需要在后台执行基于网络或CPU密集型任务并且不阻止当前线程时,您需要使用它。
以下是有关如何以及何时使用各种队列的快速指南async
:
-
主队列:这是在完成并发队列任务中的工作后更新UI的常用选择。要做到这一点,你会在另一个代码中编写一个闭包。定位主队列并调用,以
async
保证此新任务将在当前方法结束后的某个时间执行。 - 全局队列:这是在后台执行非UI工作的常用选择。
-
自定义串行队列:当您想要连续执行后台工作并跟踪时,这是一个不错的选择。这消除了资源争用,因为您一次只知道一个任务正在执行。请注意,如果您需要方法中的数据,则必须内联另一个闭包以检索它或考虑使用
sync
。
延迟任务执行
DispatchQueue
允许您延迟任务执行。应该注意不要使用它来解决竞争条件或其他时间错误。当您想要任务在特定时间运行时使用此功能。
考虑一下你的应用的用户体验。用户可能会对第一次打开应用程序时应该怎么做感到困惑 - 是吗?:]
如果没有任何照片,向用户显示提示是个不错的主意。您还应该考虑用户的眼睛如何导航主屏幕。如果您显示的提示太快,他们可能会错过它,因为他们的眼睛萦绕在视图的其他部分。显示提示前一秒延迟应足以吸引用户的注意力并引导他们。
打开PhotoCollectionViewController.swift并填写实施showOrHideNavPrompt()
:
let delayInSeconds = 1.0 // 1
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { // 2
let count = PhotoManager.sharedManager.photos.count
if count > 0 {
self.navigationItem.prompt = nil
} else {
self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
}
}
以上是上述情况:
- 您可以指定一个变量来延迟时间。
- 然后等待指定的时间,然后异步运行更新照片数量的块并更新提示。
showOrHideNavPrompt()
执行viewDidLoad()
和你随时UICollectionView
重新加载。
构建并运行应用程序。显示提示之前应该稍微延迟一下:
想知道什么时候适合使用asyncAfter
?通常在主队列中使用它是一个不错的选择。asyncAfter
在其他队列(如全局后台队列或自定义串行队列)上使用时,您需要谨慎使用。你最好坚持主队列。
为什么不使用Timer?如果您有重复的任务更容易计划,您可以考虑使用Timer
。这里有两个理由来坚持调度队列的 asyncAfter。。
一个是可读性。要使用Timer
你必须定义一个方法,然后用选择器或调用定义的方法创建计时器。有了DispatchQueue
,asyncAfter
你只需添加一个闭包。
Timer
计划在运行循环中,因此您还必须确保它在您希望启动的运行循环上进行调度(并且在某些情况下,为了正确的运行循环模式)。在这方面,使用调度队列更容易。
管理单例
单例,喜欢它们或者讨厌它们,它们在iOS上很受欢迎,就像网络上的猫照片一样。:]
单例经常关心的是他们经常不是线程安全的。考虑到它们的使用,这种担心是合理的:singletons通常用于同时访问单例实例的多个控制器。你的PhotoManager类是单例,所以你需要考虑这个问题。
线程安全代码可以安全地从多线程或并发任务中调用,而不会导致任何问题,如数据损坏或应用程序崩溃。不是线程安全的代码一次只能在一个上下文中运行。
在单例实例的初始化期间以及读取和写入实例期间,需要考虑两个线程安全情况。
由于Swift初始化全局变量,初始化变成了简单的情况。全局变量在首次访问时被初始化,并且它们保证以原子方式初始化。也就是说,执行初始化的代码被视为临界区,并且在任何其他线程访问全局变量之前保证完成。
关键部分是一段不能并发执行的代码,即一次执行两个线程。这通常是因为代码操纵共享资源,例如变量,如果它被并发进程访问,它可能会损坏。
打开PhotoManager.swift以查看单例是如何初始化的:
private let _sharedManager = PhotoManager()
私有全局_sharedManager变量用于PhotoManager懒惰地初始化。这只发生在您可以在此处看到的第一次访问:
class var sharedManager:PhotoManager {
return _sharedManager
}
公共sharedManager变量返回私有_sharedManager变量。Swift确保这个操作是线程安全的。
在访问处理共享内部数据的单例中的代码时,您仍然必须处理线程安全。您可以通过同步数据访问等方法来处理此问题。你会在下一节看到一种方法。
处理读者 - 作家问题
在Swift中,用let
关键字声明的任何变量都被认为是常量,并且是只读的和线程安全的。var
然而,用关键字声明变量,并且它变得可变并且不是线程安全的,除非数据类型被设计成这样。swift集合类型,如Array
和Dictionary
不是线程安全的当声明可变时。
尽管许多线程可以Array
同时读取一个可变实例而不会出现问题,但在另一个线程正在读取数据时让一个线程修改数组并不安全。你的单例并不妨碍这种情况发生在目前的状态。
要查看问题,请addPhoto(_:)
在PhotoManager.swift中查看,其中转载如下:
func addPhoto(_ photo: Photo) {
_photos.append(photo)
DispatchQueue.main.async {
self.postContentAddedNotification()
}
}
这是一个写入方法,因为它修改了一个可变数组对象。
现在看看这个photos
属性,转载如下:
fileprivate var _photos: [Photo] = []
var photos: [Photo] {
return _photos
}
该属性的getter在读取可变数组时被称为读取方法。调用者获取数组的一个副本,并受到保护,以防止不适当地改变原始数组。这不会对一个线程调用write方法提供任何保护addPhoto(_:)
,同时另一个线程调用该photos
属性的getter 。
*注意:*在上面的代码中,为什么调用者获取`photos`数组的副本?在Swift中,函数的参数和返回类型是通过引用或按值传递的。
按值传递会导致对象的副本,并且对副本的更改不会影响原始副本。默认情况下,Swift类实例通过引用传递,结构通过值传递。Swift的内置数据类型如,`Array`和`Dictionary`,被实现为结构体。
它可能看起来像在代码中来回传递集合时有很多复制。不要担心这个内存使用的影响。Swift集合类型经过优化,只在必要时才进行复制,例如,在传递数值时第一次修改数组。
这是经典的软件开发读者 - 作家问题。GCD提供了使用调度障碍创建读/写锁的优雅解决方案。调度障碍是一组函数,当与并发队列一起工作时,它们充当了串行风格的瓶颈。
当您提交一个DispatchWorkItem
派遣队列时,您可以设置标志来指示它应该是在该特定时间在指定队列上执行的唯一项目。这意味着所有在派送屏障前提交给队列的项目必须在DispatchWorkItem
意愿执行之前完成。
当DispatchWorkItem
轮到到达,屏障执行它,并确保在此期间,队列不执行任何其他任务。完成后,队列返回到其默认实现。
下图说明了屏障对各种异步任务的影响:
请注意,在正常操作中,队列的行为如同正常的并发队列。但是,当屏障执行时,它本质上就像一个串行队列。也就是说,障碍是唯一执行的事情。屏障完成后,队列返回正常并发队列。
在全局后台并发队列中使用障碍时要小心,因为这些队列是共享资源。在自定义串行队列中使用障碍是多余的,因为它已经连续执行。在自定义并发队列中使用障碍是处理代码关键区域原子中的线程安全的绝佳选择。
您将使用自定义并发队列来处理您的屏障功能并分离读取和写入功能。并发队列将允许同时进行多个读取操作。
打开PhotoManager.swift并在_photos
声明的上方添加一个私有属性:
fileprivate let concurrentPhotoQueue = DispatchQueue(label: "com.raywenderlich.googlyPuff.photoQueue", attributes: .concurrent)
这初始化concurrentPhotoQueue
为并发队列。
- 您设置了
label
一个在调试过程中很有用的描述性名称。通常,您将使用反转的DNS样式命名约定。 - 您指定一个并发队列。
接下来,addPhoto(_:)
用下面的代码替换:
func addPhoto(_ photo: Photo) {
concurrentPhotoQueue.async(flags: .barrier) { // 1
self._photos.append(photo) // 2
DispatchQueue.main.async { // 3
self.postContentAddedNotification()
}
}
}
以下是您的新写入功能的工作原理:
- 您可以使用屏障异步发送写操作。它执行时,它将成为队列中唯一的项目。
- 您将该对象添加到数组。
- 最后,你发布了一条通知,说明你已经添加了照片。这个通知应该发布在主线程上,因为它会做UI工作。因此,您将另一个任务异步分派到主队列以触发通知。
这需要处理写操作,但您也需要实现photos
读取方法。
为了确保写入时的线程安全,您需要在concurrentPhotoQueue
队列上执行读取操作。你需要从函数调用返回数据,所以异步调度不会削减它。在这种情况下,sync
将是一个很好的候选人。
用于sync
通过调度屏障跟踪工作情况,或者在需要等待操作完成时才能使用由闭包处理的数据。
你需要小心,虽然。想象一下,如果你打电话给sync
目标队列,你已经在运行。这将导致死锁情况。
如果两个(或有时更多)项目(大多数情况下是线程)被阻塞,如果他们都等待对方完成或执行另一个操作。第一个无法完成,因为它正在等待第二个完成。但第二个无法完成,因为它正在等待第一个完成。
在你的情况下,这个sync
调用将一直等到闭包完成,但闭包不能完成(它甚至不能启动!),直到当前正在执行的闭包完成,这不能!这应该强制你意识到你在呼叫哪个队列 - 以及你通过哪个队列。
以下简要介绍何时何地使用sync
:
- 主队列:出于与上述相同的原因非常小心; 这种情况也有可能造成死锁。
- 全局队列:这是通过调度屏障或等待任务完成时同步工作的好候选者,因此您可以执行进一步处理。
-
自定义串行队列:在这种情况下非常小心; 如果你在一个队列中运行,并且调用
sync
目标队列,你肯定会造成死锁。
仍然在PhotoManager.swift中修改photos
属性getter:
var photos: [Photo] {
var photosCopy: [Photo]!
concurrentPhotoQueue.sync { // 1
photosCopy = self._photos // 2
}
return photosCopy
}
以下是一步一步做的事情:
- 同步分派
concurrentPhotoQueue
到执行读取。 - 存储照片数组的副本
photosCopy
并将其返回。
构建并运行应用程序。通过Le Internet选项下载照片。它应该像以前一样行动,但是在引擎盖下,你有一些非常快乐的线程。
恭喜你 - 你的PhotoManager
单例现在是线程安全的。无论你在哪里或如何阅读或写照片,你都可以确信,这将以安全的方式完成,不会有任何意外。
然后去哪儿?
在这个Grand Central Dispatch教程中,您学习了如何使您的代码线程安全以及如何在执行CPU密集型任务时保持主线程的响应速度。
您可以下载已完成的项目,其中包含迄今为止在本教程中所做的所有改进。在本教程的第二部分中,您将继续改进此项目。
如果您打算优化自己的应用程序,那么您应该使用乐器中的时间配置文件模板来分析您的工作。使用此实用程序超出了本教程的范围,因此请查看如何使用仪器进行精彩的概述。
此外,请确保您使用实际设备进行配置文件,因为在模拟器上进行测试可能会得出与用户体验不同的结果。
我们的iOS并发与GCD和操作视频教程系列也涵盖了本教程中介绍的许多相同主题。
在本教程的下一部分中,您将深入研究GCD的API,以做更多更酷的事情。
如果您有任何问题或意见,请随时加入下面的讨论!
网友评论