Grand Central Dispatch(简称GCD)是Swift开发人员多次使用的基本技术之一。它主要以能够在不同的并发队列上分派工作而闻名,并且经常被用来编写这样的代码:
DispatchQueue.main.async {
// Run async code on the main queue
}
但如果我们再深入一点,就会发现GCD还有一套真正强大的api和特性,但不是每个人都知道。本周,让我们超越async{},看看GCD在哪些情况下真的很有用,以及它如何为许多其他更常见的基础api提供更简单(和更“快速”)的选项。
Delaying a cancellable task with DispatchWorkItem
关于GCD的一个常见误解是“一旦你安排了一个任务,它就不能被取消,你需要使用Operation这个操作API”。这在过去是正确的,随着iOS 8和macOS 10.10引入了DispatchWorkItem,它在一个非常容易使用的API中提供了精确的功能。
假设我们的UI有一个搜索栏,当用户输入一个字符时,我们通过调用后端来执行搜索。因为用户可以快速输入,所以我们不想马上启动网络请求(这可能会浪费大量数据和服务器容量),相反,我们将“debounce”这些事件,只在用户在0.25秒内没有输入时执行请求。
这就是DispatchWorkItem的作用所在。 通过将我们的请求代码封装在一个工作项中,我们可以很容易地在它被一个新的替换时取消它,像这样:
class SearchViewController: UIViewController, UISearchBarDelegate {
// We keep track of the pending work item as a property
private var pendingRequestWorkItem: DispatchWorkItem?
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// Cancel the currently pending item
pendingRequestWorkItem?.cancel()
// Wrap our request in a work item
let requestWorkItem = DispatchWorkItem { [weak self] in
self?.resultsLoader.loadResults(forQuery: searchText)
}
// Save the new work item and execute it after 250 ms
pendingRequestWorkItem = requestWorkItem
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250),
execute: requestWorkItem)
}
}
正如我们上面所看到的,在Swift中使用DispatchWorkItem实际上比使用计时器或Operation要简单和漂亮得多,这要归功于尾随闭包语法和将GCD导入Swift的良好性能。 我们不需要@objc标记的方法或#选择器——这些都可以用闭包来完成。
Grouping and chaining tasks with DispatchGroup
有时我们需要执行一组操作,然后才能继续我们的逻辑。例如,假设我们需要在创建模型之前从一组数据源加载数据。不必自己跟踪所有的数据源,我们可以很容易地与DispatchGroup同步工作。
使用分派组还有一个很大的优势,那就是我们的任务可以在单独的队列中并发地运行。这使我们能够从简单的开始,然后在需要时轻松地添加并发性,而不必重写任何任务。 我们所要做的就是平衡调用调度组上的enter()和leave(),让它同步我们的任务。
让我们看一个例子,在这个例子中,我们从本地存储、iCloud驱动器和后端系统加载笔记,然后将所有的结果合并成一个NoteCollection:
// First, we create a group to synchronize our tasks
let group = DispatchGroup()
// NoteCollection is a thread-safe collection class for storing notes
let collection = NoteCollection()
// The 'enter' method increments the group's task count…
group.enter()
localDataSource.load { notes in
collection.add(notes)
// …while the 'leave' methods decrements it
group.leave()
}
group.enter()
iCloudDataSource.load { notes in
collection.add(notes)
group.leave()
}
group.enter()
backendDataSource.load { notes in
collection.add(notes)
group.leave()
}
// This closure will be called when the group's task count reaches 0
group.notify(queue: .main) { [weak self] in
self?.render(collection)
}
上面的代码可以工作,但是有很多重复的地方。让我们把它重构成一个数组扩展,使用数据源协议作为其元素类型的同类型约束:
extension Array where Element == DataSource {
func load(completionHandler: @escaping (NoteCollection) -> Void) {
let group = DispatchGroup()
let collection = NoteCollection()
// De-duplicate the synchronization code by using a loop
for dataSource in self {
group.enter()
dataSource.load { notes in
collection.add(notes)
group.leave()
}
}
group.notify(queue: .main) {
completionHandler(collection)
}
}
}
有了上面的扩展,我们现在可以将之前的代码简化为:
let dataSources: [DataSource] = [
localDataSource,
iCloudDataSource,
backendDataSource
]
dataSources.load { [weak self] collection in
self?.render(collection)
}
造工十分札实精致;
Waiting for asynchronous tasks with DispatchSemaphore
DispatchGroup提供了一种简单的方法来同步一组异步操作,同时仍然保持异步状态,而DispatchSemaphore则提供了一种同步等待一组异步任务的方法。这在命令行工具或脚本中非常有用,因为我们没有应用程序run loop,而只是在全局上下文中同步执行,直到完成。
像DispatchGroup一样,semaphore API非常简单,我们只通过调用wait()或signal()来增加或减少一个内部计数器。在signal()之前调用wait()将阻塞当前队列,直到接收到信号。
让我们在之前的扩展数组上创建另一个重载,它以同步方式返回一个NoteCollection,或者抛出一个错误。我们将重用之前基于dispatchgroup的代码,但只是使用一个semaphore来协调该任务。
extension Array where Element == DataSource {
func load() throws -> NoteCollection {
let semaphore = DispatchSemaphore(value: 0)
var loadedCollection: NoteCollection?
// We create a new queue to do our work on, since calling wait() on
// the semaphore will cause it to block the current queue
let loadingQueue = DispatchQueue.global()
loadingQueue.async {
// We extend 'load' to perform its work on a specific queue
self.load(onQueue: loadingQueue) { collection in
loadedCollection = collection
// Once we're done, we signal the semaphore to unblock its queue
semaphore.signal()
}
}
// Wait with a timeout of 5 seconds
semaphore.wait(timeout: .now() + 5)
guard let collection = loadedCollection else {
throw NoteLoadingError.timedOut
}
return collection
}
}
在Array上使用上述新方法,我们现在可以像这样在脚本或命令行工具中同步加载notes:
let dataSources: [DataSource] = [
localDataSource,
iCloudDataSource,
backendDataSource
]
do {
let collection = try dataSources.load()
output(collection)
} catch {
output(error)
}
Observing changes in a file with DispatchSource
我想提到的GCD的最后一个“不太为人所知”的特性是它如何提供一种方法来观察文件系统中文件的变化。 与DispatchSemaphore一样,如果我们想自动对用户编辑的文件做出反应,这在脚本或命令行工具中非常有用。 这使我们能够轻松地构建具有“实时编辑”特性的开发工具。
DispatchSource有几种不同的变体,这取决于我们想要观察什么。在本例中,我们将使用DispatchSourceFileSystemObject,它允许我们观察来自文件系统的事件。
让我们看一个简单FileObserver的示例实现,它允许我们附加一个闭包,以便在每次给定文件发生更改时运行。它的工作原理是使用文件描述符和DispatchQueue创建一个dispatch source来执行观察,并使用文件来引用要观察的文件:
class FileObserver {
private let file: File
private let queue: DispatchQueue
private var source: DispatchSourceFileSystemObject?
init(file: File) {
self.file = file
self.queue = DispatchQueue(label: "com.myapp.fileObserving")
}
func start(closure: @escaping () -> Void) {
// We can only convert an NSString into a file system representation
let path = (file.path as NSString)
let fileSystemRepresentation = path.fileSystemRepresentation
// Obtain a descriptor from the file system
let fileDescriptor = open(fileSystemRepresentation, O_EVTONLY)
// Create our dispatch source
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fileDescriptor,
eventMask: .write,
queue: queue
)
// Assign the closure to it, and resume it to start observing
source.setEventHandler(handler: closure)
source.resume()
self.source = source
}
}
我们现在可以像这样使用FileObserver:
let observer = try FileObserver(file: file)
observer.start {
print("File was changed")
}
想象一下,所有很酷的开发工具都可以用它来构建!😀
Conclusion
Grand Central Dispatch是一个非常强大的框架,它的功能远远超过了它最初看起来的样子。希望这篇文章能激发你的想象力,让你知道你可以用它来做什么,我建议你下次需要做我们在这篇文章中提到的任务时尝试一下。
在我看来,很多基于计时器或操作队列的代码,以及第三方异步框架的使用,实际上可以通过直接使用GCD来简化。