版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.08.20 |
前言
信号量机制是多线程通信中的比较重要的一部分,对于
NSOperation
可以设置并发数,但是对于GCD
就不能设置并发数了,那么就只能靠信号量机制了。接下来这几篇就会详细的说一下并发机制。感兴趣的可以看这几篇文章。
1. iOS与多线程(一) —— GCD中的信号量及几个重要函数
2. iOS与多线程(二) —— NSOperation实现多并发之创建任务
3. iOS与多线程(三) —— NSOperation实现多并发之创建队列和开启线程
4. iOS与多线程(四) —— NSOperation的串并行和操作依赖
5. iOS与多线程(五) —— GCD之一个简单应用示例(一)
开始
本篇主要将深入研究高级GCD概念,包括调度组,取消调度块,异步测试技术和调度源。
如果您继续学习,您可以从上一篇的示例项目中选择您上次停下的地方。
运行应用程序,点击+,然后选择Le Internet
以添加互联网照片。 您可能会注意到在图像下载完成之前会弹出下载完成alert消息:
这个就是首先你需要解决的问题。
Dispatch Groups - 调度组
打开PhotoManager.swift
并查看downloadPhotos(withCompletion :)
:
func downloadPhotos(
withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError?
for address in [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces] {
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
}
PhotoManager.shared.addPhoto(photo)
}
completion?(storedError)
}
传递给方法的completion
闭包触发alert弹窗。您可以在下载照片的for循环后调用此方法。在调用闭包之前,您错误地认为下载已完成。
您通过调用DownloadPhoto(url :)
开始下载照片。此调用立即返回,但实际下载是异步发生的。因此,当完成运行时,无法保证所有下载都已完成。
你想要的是downloadPhotos(withCompletion :)
在所有照片下载任务完成后调用它的completion
闭包。如何监控这些并发异步事件来实现这一目标?使用当前的方法,您不知道任务何时完成,并且可以按任何顺序完成。
好消息!这正是调度组(dispatch groups )
存在的原因。使用调度组,您可以将多个任务组合在一起,并等待它们完成,或者在完成后收到通知。任务可以是异步的或同步的,甚至可以在不同的队列上运行。
DispatchGroup
管理调度组。你将首先看看它的wait
方法。这将阻塞当前线程,直到所有组的排队任务完成。
在PhotoManager.swift
中,用以下代码替换downloadPhotos(withCompletion :)
中的代码:
// 1
DispatchQueue.global(qos: .userInitiated).async {
var storedError: NSError?
// 2
let downloadGroup = DispatchGroup()
for address in [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces] {
let url = URL(string: address)
// 3
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
// 4
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
// 5
downloadGroup.wait()
// 6
DispatchQueue.main.async {
completion?(storedError)
}
}
以下是代码逐步执行的操作:
- 1)由于您正在使用阻塞当前线程的同步
wait
方法,因此使用async
将整个方法放入后台队列以确保不阻止主线程。 - 2)创建一个新的调度组。
- 3)调用
enter()
手动通知组任务已启动。您必须使用和leave()
数量相等进行调用enter()
,否则您的应用程序将崩溃。 - 4)在这里,您通知小组这项工作已完成。
- 5)在等待任务完成时调用
wait()
来阻塞当前线程。这等待会持续,因为照片创建任务总是完成。您可以使用wait(timeout :)
来指定超时,并在指定时间后等待挽救。 - 6)此时,您可以保证所有图像任务都已完成或超时。然后,您回调主队列以运行完成关闭。
Build并运行应用程序。通过Le Internet
选项下载照片,并验证在下载所有图像之前alert弹窗未显示。
注意:如果网络活动发生得太快以至于无法识别何时应该调用完成闭包并且您在设备上运行应用程序,则可以通过在iOS的
Settings
应用程序的Developer
部分中切换某些网络设置来确保这确实有效。 只需转到Network Link Conditioner
部分,启用它,然后选择配置文件。Very Bad Network
是一个不错的选择。如果您在模拟器上运行,则可以使用 Network Link Conditioner included in the Advanced Tools for Xcode来更改网络速度。 这是一个很好的工具,因为它会让你在连接速度低于最佳状态时意识到你的应用程序会发生什么。
Dispatch groups
是所有类型队列的很好的选择。 如果您因为不想保留主线程而同步等待完成所有工作,那么您应该注意在主队列上使用调度组。 但是,异步模型是一种在多个长时间运行的任务完成后更新UI的一个不错的方法,例如网络调用。
您当前的解决方案是好的,但一般来说,最好尽可能避免阻塞线程。 您的下一个任务是重写相同的方法,以便在完成所有下载后异步通知您。
Dispatch Groups, Take 2 - 调度组(2)
异步调度到另一个队列然后使用wait
阻塞工作是不可取的。 幸运的是,有一种更好的方法。 DispatchGroup
可以在所有组的任务完成时通知您。
仍然在PhotoManager.swift
中,用以下内容替换downloadPhotos(withCompletion :)
中的代码:
// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces] {
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
// 2
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
下面进行详细拆分:
- 1)在这个新实现中,您不需要在异步
(async)
调用中包围该方法,因为您没有阻塞主线程。 - 2)
notify(queue:work :)
用作异步完成闭包。 它在组中没有剩余项目时运行。 您还指定要计划在主队列上调度完成工作。
这是一种更清洁的方式来处理这个特定的工作,因为它不会阻止任何线程。
Build并运行应用程序。 确认在下载所有互联网照片后仍然显示下载完成alert弹窗:
Concurrency Looping - 并发循环
有了所有这些新工具,你可能应该解决所有问题,对吧!?
看看PhotoManager
中的downloadPhotos(withCompletion :)
。 您可能会注意到那里有一个for
循环,循环三次迭代并下载三个单独的图像。 你的工作是看你是否可以同时运行这个for
循环来尝试加快速度。
这是DispatchQueue.concurrentPerform(iterations:execute:)
的工作。 它与for循环的工作方式类似,因为它同时执行不同的迭代。 它是同步的,只有在完成所有工作后才返回。
在确定给定工作量的最佳迭代次数时,您必须小心。 每次迭代的许多迭代和少量工作都会产生如此多的开销,从而抵消了使调用并发的任何好处或者优势。 称为跨步(striding)
的技术可以帮助你。 Striding
允许您为每次迭代执行多项工作。
什么时候适合使用DispatchQueue.concurrentPerform(iterations:execute :)?
您可以排除串行队列,因为那里没有任何好处 - 您也可以使用普通的for循环。 对于包含循环的并发队列来说,这是一个不错的选择,特别是如果您需要跟踪进度。
在PhotoManager.swift
中使用以下代码替换downloadPhotos(withCompletion :)
中的代码:
var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
let address = addresses[index]
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
您使用DispatchQueue.concurrentPerform(iterations:execute :)
替换前者for
循环以处理并发循环。
这个实现包括一段奇怪的代码:let _ = DispatchQueue.global(qos:.userInitiated)
。 调用此方法会导致GCD使用具有.userInitiated
服务质量的队列进行并发调用。
Build并运行应用程序。 验证Internet下载功能是否仍然正常运行:
在设备上运行此新代码有时会产生稍微快一些的结果。但这一切都值得吗?
实际上,在这种情况下,它是不值得的。原因如下:
- 与首先运行for循环相比,您可能创建了更多的并行运行线程的开销,您应该使用
DispatchQueue.concurrentPerform(iterations:execute :)
来迭代非常大的集合以及适当的跨度长度。 - 您创建应用程序的时间有限 - 不要浪费时间预先优化您不知道的代码。如果你要优化某些东西,那么优化一些值得注意的东西,值得花时间。通过在Instruments中分析您的应用程序,找到执行时间最长的方法。
- 通常,优化代码会使您的代码对您自己以及其他开发人员变得更加复杂,确保增加的复杂度是值得的。
请记住,不要因为优化而疯狂。你只会让自己和那些不得不研究你的代码的人变得更难。
Canceling Dispatch Blocks - 取消调度块
到目前为止,您还没有看到允许您取消排队任务的代码。 这是DispatchWorkItem
表示的dispatch block objects
成为焦点的地方。 请注意,您只能在DispatchWorkItem
到达队列头部并开始执行之前取消它。
让我们通过从Le Internet
开始几个图像的下载任务然后取消其中一些来证明这一点。
仍在PhotoManager.swift
中,将downloadPhotos(withCompletion :)
中的代码替换为以下代码:
var storedError: NSError?
let downloadGroup = DispatchGroup()
var addresses = [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces]
// 1
addresses += addresses + addresses
// 2
var blocks: [DispatchWorkItem] = []
for index in 0..<addresses.count {
downloadGroup.enter()
// 3
let block = DispatchWorkItem(flags: .inheritQoS) {
let address = addresses[index]
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
blocks.append(block)
// 4
DispatchQueue.main.async(execute: block)
}
// 5
for block in blocks[3..<blocks.count] {
// 6
let cancel = Bool.random()
if cancel {
// 7
block.cancel()
// 8
downloadGroup.leave()
}
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
以下是逐步完成上述代码的步骤:
- 1)您展开地址
(addresses)
数组以保存每个图像的三个副本。 - 2)初始化块
(blocks)
数组以保存调度块对象供以后使用。 - 3)您创建一个新的
DispatchWorkItem
。传入flags
参数以指定块应从您将其分派到的队列中继承其Quality of Service
类。然后,定义要在闭包中执行的工作。 - 4)您将块异步调度到主队列。对于此示例,使用主队列可以更轻松地取消选择块,因为它是一个串行队列。设置调度块的代码已在主队列上执行,因此可以保证下载块将在以后执行。
- 5)通过切片块
blocks
数组跳过前三个下载块。 - 6)在这里,您使用
Bool.random()
在true和false之间随机选择。这就像掷硬币一样。 - 7)如果随机值为true,则取消该块。这只能取消仍在队列中但尚未开始执行的块。您无法在执行过程中取消块。
- 8)在这里,您需要记住从调度组中删除已取消的块。
Build并运行应用程序,然后从Le Internet
添加图像。你会看到该应用程序现在下载了三个以上的图像。每次重新运行应用程序时,额外图像的数量都会发生变化。您在开始之前取消队列中的一些其他图像下载。
这是一个特意做的例子,但它很好地说明了如何使用和取消调度块。
调度块可以执行更多操作,因此请务必查看Apple's documentation。
Miscellaneous GCD Fun - 其他GCD乐趣
还有更多! 这里有一些额外的功能。 虽然您几乎不会频繁使用这些工具,但它们在正确的情况下可以提供极大的帮助。
1. Testing Asynchronous Code - 测试异步代码
这可能听起来像一个疯狂的想法,但你知道Xcode有测试功能吗? 我知道,有时我喜欢假装它不存在,但在代码中构建复杂的关系时,编写和运行测试很重要。
Xcode测试都包含在XCTestCase
的子类中,并且是签名以test
开头的任何方法。 Tests
在主线程上运行,因此您可以假设每个测试都以串行方式进行。
一旦给定的测试方法完成,Xcode就会认为测试已经完成并继续进行下一个测试。 这意味着在下一个测试运行时,前一个测试的任何异步代码都将继续运行。
网络代码通常是异步的,因为您不希望在执行网络提取时阻塞主线程。 这与测试方法完成后测试完成的事实相结合,可能使测试网络代码变得困难。
我们来简要介绍一下如何使用信号量(semaphores)
来测试异步代码。
2. Semaphores - 信号量
信号量是一个老式的线程概念,由Edsger W. Dijkstra
引入。 信号量是一个复杂的话题,因为它们建立在操作系统功能的复杂性之上。
如果您想了解有关信号量的更多信息,请查看有关信号量理论的detailed discussion。 如果你是学术类型,你可能想看看Dining Philosophers Problem,这是一个使用信号量的经典软件开发问题。
打开GooglyPuffTests.swift
并使用以下代码替换downloadImageURL(withString :)
中的代码:
let url = URL(string: urlString)
// 1
let semaphore = DispatchSemaphore(value: 0)
let _ = DownloadPhoto(url: url!) { _, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
// 2
semaphore.signal()
}
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)
// 3
if semaphore.wait(timeout: timeout) == .timedOut {
XCTFail("\(urlString) timed out")
}
以下是信号量在上面的代码中的工作原理:
- 1)您创建一个信号量并设置其起始值。 这代表可以访问信号量而不需要增加信号量的事物数量(请注意,递增信号量称为发信号)。
- 2)您在完成闭包中发出信号量的信号。 这会增加信号量计数,并表示信号量可供其他需要它的资源使用。
- 3)你等待信号量,给定超时。 此调用会阻塞当前线程,直到信号量发出信号。 此函数的非零返回码表示超时时间已到期。 在这种情况下,测试失败,因为网络返回的时间不应超过10秒。
如果您具有默认键绑定,则从菜单中选择Product ▸ Test
或使用Command-U
运行测试。 他们都应该及时取得成功:
停止连接并再次运行测试。 如果您在设备上运行,请将其置于飞行(airplane)
模式。 如果您在模拟器上运行,则只需关闭连接即可。 测试在10秒后完成,可以看见失败结果。 很棒,它有效!
这些都是相当简单的测试,但如果您正在与服务器团队合作,这些基本测试可以防止一轮指责谁应该为最新的网络问题负责。
注意:在代码中实现异步测试时,请先查看XCTWaiter,然后再转到这些低级API。
XCTWaiter
的API更好,为异步测试提供了许多强大的技术。
3. Dispatch Sources
Dispatch sources是GCD特别有趣的功能。 您可以使用调度源来监视某种类型的事件。 事件可以包括Unix信号,文件描述符,Mach端口,VFS节点和其他模糊的东西。
在设置调度源时,您可以告诉它要监视的事件类型以及应在其上执行事件处理程序块的调度队列。 然后,您将事件处理程序分配给调度源。
创建后,调度源将以挂起状态启动。 这允许您执行所需的任何其他配置,例如设置事件处理程序。 配置调度源后,必须将其恢复以开始处理事件。
在本文中,您将以一种相当特殊的方式来尝试使用调度源:监视应用程序何时进入调试模式(debug mode)
。
打开PhotoCollectionViewController.swift
并在backgroundImageOpacity
全局属性声明下面添加以下内容:
// 1
#if DEBUG
// 2
var signal: DispatchSourceSignal?
// 3
private let setupSignalHandlerFor = { (_ object: AnyObject) in
let queue = DispatchQueue.main
// 4
signal =
DispatchSource.makeSignalSource(signal: SIGSTOP, queue: queue)
// 5
signal?.setEventHandler {
print("Hi, I am: \(object.description!)")
}
// 6
signal?.resume()
}
#endif
下面进行详细说明:
- 1)您只能在DEBUG模式下编译此代码,以防止
“interested parties”
获得对您的应用程序的大量洞察。DEBUG是通过在Project Settings -> Build Settings -> Swift Compiler - Custom Flags -> Other Swift Flags -> Debug
下添加-D DEBUG
来定义的。它应该已经在启动项目中设置。 - 2)您声明一个
DispatchSourceSignal
类型的signal
变量,用于监视Unix
信号。 - 3)您创建一个分配给
setupSignalHandlerFor
全局变量的块,您将用于一次性设置调度源。 - 4)在这里设置
signal
。您表示您有兴趣监视SIGSTOP
Unix信号并处理主队列上收到的事件 - 您很快就会发现原因。 - 5)如果成功创建了调度源,则会注册每当收到
SIGSTOP
信号时调用的事件处理程序闭包。您的处理程序打印包含类描述的消息。 - 6)默认情况下,所有源都以挂起状态启动。在这里,您告诉调度源恢复,以便它可以开始监视事件。
将以下代码添加到对super.viewDidLoad()
调用正下方的viewDidLoad()
中:
#if DEBUG
setupSignalHandlerFor(self)
#endif
此代码调用调度源的初始化代码。
Build并运行应用程序。 通过点击Xcode调试器中的暂停然后播放按钮,暂停程序执行并立即恢复应用程序:
看一下控制台,你会看到这些输出:
Hi, I am: <GooglyPuff.PhotoCollectionViewController: 0x7fbf0af08a10>
你的app现在可以调试了! 这真是太棒了,但是你会在现实生活中如何使用它?
您可以使用它来调试对象并在您恢复应用程序时显示数据。 当恶意攻击者将调试器附加到您的应用程序时,您还可以为应用程序提供自定义安全逻辑以保护自身(或用户的数据)。
一个有趣的想法是使用此方法作为堆栈跟踪工具来查找要在调试器中操作的对象。
想想这种情况一秒钟。 当你突然停止调试器时,你几乎从不在所需的堆栈帧上。 现在,您可以随时停止调试器,并在所需位置执行代码。 如果您想在应用程序中从调试器访问繁琐的点执行代码,这非常有用。 试试看!
在刚刚添加的setupSignalHandlerFor
块内的print()
语句上放置一个断点。
在调试器中暂停,然后重新开始。 该应用程序将达到您添加的断点。 您现在深入了解PhotoCollectionViewController
方法的深度。 现在,您可以访问PhotoCollectionViewController
的实例到您的内容。 非常方便!
注意:如果您还没有注意到调试器中有哪些线程,请立即查看它们。 主线程将始终是第一个线程,然后是libdispatch,GCD的协调器,作为第二个线程。 之后,线程计数和剩余线程取决于应用程序到达断点时硬件正在执行的操作。
在控制台输入下面内容:
expr object.navigationItem.prompt = "WOOT!"
Xcode调试器有时可能不合作。 如果你收到消息:
error: use of unresolved identifier 'self'
然后你必须以艰难的方式解决LLDB
中的错误。 首先记下调试区域中object
的地址:
po object
然后通过在调试器中运行以下命令手动将值转换为所需的类型,将0xHEXADDRESS
替换为输出的地址:
expr let $vc = unsafeBitCast(0xHEXADDRESS, to: GooglyPuff.PhotoCollectionViewController.self)
expr $vc.navigationItem.prompt = "WOOT!"
如果这不起作用,幸运的是 - 你在LLDB中遇到了另一个错误! 在这种情况下,您可能需要再次尝试构建和运行应用程序。
成功运行此命令后,请继续执行应用程序。 你会看到以下内容:
使用此方法,您可以对UI进行更新,查询类的属性,甚至执行方法 - 所有这些都可以在不必重新启动应用程序的情况下进入特殊的工作流状态。 很简约。
除了GCD,通常,如果您使用简单的fire-and-forget
任务,最好使用GCD。 Operation
提供了更好的控制,处理最大并发操作的实现,以及以速度为代价的更加面向对象的范例。
请记住,除非您有特定的理由要求降低,否则请始终尝试使用更高级别的API。
后记
本篇主要讲述了,感兴趣的给个赞或者关注~~~