Swift 3 之 GCD 教程(二)【译】

原文:Grand Central Dispatch Tutorial for Swift 3: Part 2/2

欢迎来到 GCD 系列教程的第二篇,也是最后一篇。

在该系列的第一篇原文链接)中,你学习了关于并发、线程以及怎么使用 GCD。你为读出图片实现了一个单例,并且利用 dispatch barriers 与 synchronous dispatch queues 决了读者-写者问题以达到线程安全。同时,你使用 dispatch queues 来延迟提示信息的显示来提高 app 的用户体验,以及当初始化 ViewController 时异步分流 CPU 密集工作。

在这一篇的 GCD 教程中,同样会使到上篇中的 GooglyPuff app。你将深入研究高级的 GCD 概念,包括 dispatch groups、取消 dispatch blocks、异步测试技术以及 dispatch sources。

是时候探索更多的GCD了!

开始

你可选择继续使用上一篇教程的工程项目。或者,你可以下载准备好的项目

运行 app,点击 + 并选择 Le Internet 来添加网络照片。你应该注意到了图片还没在下载完成而 Download Complete 提示框已经提前显示出来。

这就是你需要解决的第一个问题。

Dispatch Groups

打开 PhotoManager.swift 并查看 downloadPhotosWithCompletion(_:):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func downloadPhotosWithCompletion(
_ completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError?
for address in [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString] {
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
}
PhotoManager.sharedManager.addPhoto(photo)
}
completion?(storedError)
}

提示框是通过该方法传入的参数 completion 闭包调用的。它是在图片下载的 for 循环后直接调用的。你误以为下载图片会在该闭包调用之前完成。

图片下载是通过调用 DownloadPhoto(url) 完成的。该调用的结果会立即返回,但实际上下载过程是异步的。所以当 completion 调用时,并不能保证所有的图片已经下载完成。

What you want is for downloadPhotosWithCompletion(_:) to call its completion closure after all the photo download tasks are complete. How can you monitor these concurrent asynchronous events to achieve this? With the current methodology, you don’t know when the tasks are complete and they can finish in any order.

好消息!Dispatch groups的设计就是用来解决该问题的。你可以使用 dispatch group 把多个任务组合在一起并等待它们完成,或者当它们一旦完成后收到通知。这些任务可以是异步或,甚至可以是不同队列的任务。

DispatchGroup 管理调度组。你第一个要关注的是 wait 方法。 在该 group 所有的调度任务完成之前它会阻塞当前线程。(原谅:This blocks your current thread until all the group’s enqueued tasks have been completed.)

PhotoManager.swfit 中使用下面的代码覆盖 downloadPhotosWithCompletion(_:) 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DispatchQueue.global(qos: .userInitiated).async { // 1
var storedError: NSError?
let downloadGroup = DispatchGroup() // 2
for address in [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString] {
let url = URL(string: address)
downloadGroup.enter() // 3
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
downloadGroup.leave() // 4
}
PhotoManager.sharedManager.addPhoto(photo)
}
downloadGroup.wait() // 5
DispatchQueue.main.async { // 6
completion?(storedError)
}
}

这里,一步一步的讲解上面的代码具体做了什么:

  1. 由于你使用了同步的 wait 方法会阻塞当前线程,所以你使用 async 把整个方法放入后台队列中进行,以确保该方法不会阻塞主线程。
  2. 这里你创建了一个新的 dispatch group。
  3. 你调用 enter() 来手动通知 goup 该任务已经开始了。你必须使用 leave() 的调用次数来平衡 enter()的调用次数,否则你的 app 将会崩溃。
  4. 在这里,你通知 group 该任务已经完成。
  5. 你调用 wait() 来阻塞当前线程,等待所有任务完成。这种永远的等待是好的因为 photos 创建的任务总是会完成。你可以使用 wait(timeout:) 来指定一个超时并且当超时后将不再等待。
  6. 这里,你保证所有的图片下载任务完成或者超时。然后在主线程回调上执行你的 completion 闭包。

Bulid 并运行你的 app。通过 Le Internet 选项下载图片并检查所有图片下载完成前提示框是否弹出。

笔记:TODO

Dispatch group 对于所有类型的队列来说是个很好选择。你应该警惕的是在主线程中,如果你同步的等待所有任务完成,由于你不希望占用主线程。然而,异步模型是非常好的方法用来更新 UI,尤其是在几个需要长时间运行的任务完成后更新 UI,例如网络调用。

当前的解决方法是不错的,不过在通常情况下应该尽可能的避免阻塞当前线程。下一任务是当任务完成后使用异步通知的方式来重写它。

Dispatch Groups,Take 2(第二种使用方式)

异步调度到另一个队列然后使用 wait 阻塞当前队列的做法是不够优雅的。幸好,这是有一个更好的方法。当 group 的所有任务完成时,可以使用 DispatchGroupnotify 来代替。

仍然在 PhotoManager.swift 中,使用下面的代码替换 downloadPhotosWithCompletion(_:) 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [overlyAttachedGirlfriendURLString,
successKidURLString,
lotsOfFacesURLString] {
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) {
_, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.sharedManager.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) { // 2
completion?(storedError)
}

解释下这里做了些什么:

  1. 在这新实现方法中,你不再需要使用 async 来包含这些代码,因为它并不会阻塞主线程。
  2. notify(queue:work:) 处理异步完成闭包。当 group 中的所有任务执行完成后它将被调用。你指定了 completion 任务将在主线程上运行。

这是一个非常清晰方式来处理这个特定的任务,并且它不会阻塞任何线程。

Build 并运行 app。检验一下当所有网络图片下载完成后提示框才弹出:

img -w375

Concurrency Looping (并发循环)

所有这些新的工具任你使用,你应该很使用线程来做任何事,是吗?

看一下 PhotoManager 中的 downloadPhotosWithCompletion(_:)。你应该注意到了,这里使用了 for 循环来迭代下载三张图片。你的任务是在 for 循环中尝试使用并发来加速下载。

这正好是 DispatchQueue.concurrent(iterations:execute:) 的工作。(未完待续…)