当苹果公司在2014年首次推出Swift时,它的目标是满足软件工程师对现代编程语言的所有要求。在苹果公司设计Swift的克里斯-拉特纳(Chris Lattner)的目标是制造一种既可用于教授编程又可用于为操作系统构建软件的语言。
从那时起,苹果公司将该语言开源,因此,它继续发展。尽管对Swift进行了改进,但仍然缺少一个关键功能,即并发和并行的基元。
在过去,你可以使用Grand Central Dispatch(GCD)和libdispatch等库来模仿Swift中的基元。如今,我们可以使用async 和await 关键字来执行并发性的原语。
在本教程中,我们将讨论什么是并发性,以及为什么它是有用的。然后,我们将学习如何使用async 和await 关键字来执行并发。
让我们开始吧!
并发和CPU内核
由于在过去十年中对处理器所做的改变,并发性已经成为计算机编程中一个更相关的话题。尽管较新的处理器中的晶体管数量增加了,但时钟速度并没有显著提高。
然而,处理器的一个值得注意的改进是每个芯片上有更多的CPU内核。苹果较新的处理器,如iPhone 12中的A14,有六个CPU内核。用于Mac和iPad的M1处理器有八个CPU内核。然而,A14的时钟速度仍然是3.1GHz左右。
CPU设计的真正进步来自于改变现代芯片的内核数量。为了利用这些较新的处理器,我们需要提高我们在并发式编程方面的能力。
长时间运行的任务
在大多数现代计算机系统中,主线程被用来渲染和处理用户界面和用户交互。人们经常向iOS开发者强调,永远不要阻塞主线程。
长时间运行的任务,如发出网络请求、与文件系统交互或查询数据库,都会阻塞主线程,导致应用程序的用户界面冻结。值得庆幸的是,苹果公司提供了许多不同的工具,我们可以用它们来防止应用程序的用户界面被阻塞。
Swift中的并发性选项
对GCD和libdispatch等框架的改进使并发编程变得更加容易。
目前iOS设备的最佳做法是将任何会阻塞主线程的任务卸载到后台线程或队列中。一旦任务完成,其结果通常会在一个块或尾部闭包中处理。
在GCD发布之前,苹果提供了使用委托来卸载任务的API。首先,开发者必须运行一个单独的线程到一个委托对象,该对象调用调用类上的一个方法来处理任务的完成。
虽然卸载任务是可行的,但阅读这种类型的代码可能是困难的,任何错误都允许引入新类型的错误。因此,在2017年,Chris Lattner写了他的Swift并发性宣言,表达了他对如何使用async/await在Swift中添加并发性的想法。
Grand Central Dispatch
GCD于2009年首次推出,是苹果公司通过苹果操作系统上的管理线程池管理任务并行的方法。
GCD的实现起源于一个C库,允许开发者在C、C++和Objective-C中使用它。在Swift推出后,为使用苹果较新语言的开发者创建了一个GCD的Swift包装器。
GCD也被移植到libdispatch中,它被用于其他开源软件。阿帕奇网络服务器已经纳入了这个库,用于多进程处理。
大中心DispatchQueue
让我们看看GCD的运行情况我们将使用GCD将工作分配给另一个调度队列。在下面的代码片断中,一个函数正在把它的一些工作分配给一个异步任务。
swift
func doSomethinginTheBackground() {
DispatchQueue.global(qos: .background).async {
// Do some long running work here
...
}
}
DispatchQueue 类提供了一些方法和属性,允许开发者在一个尾部闭合中运行代码。一个常见的情况是,在一个拖尾闭包中运行一个长期运行的任务,产生某种类型的结果,然后将该结果返回给主线程。
在下面的代码片段中,DispatchQueue 在将结果返回给主线程之前,正在做一些工作。
swift
DispatchQueue.global(qos: .background).async {
// Do some work here
DispatchQueue.main.async {
// return to the main thread.
print("Work completed and back on the main thread!")
}
}
更常见的情况是使用NSURLSession 进行网络调用,在一个尾部闭包中处理结果,然后返回主线程。
swift
func goGrabSomething(completion: @escaping (MyJsonModel?, Error?) -> Void) {
let ourl = URL(string: "https://mydomain.com/api/v1/getsomejsondata")
if let url = ourl {
let req = URLRequest(url: url)
URLSession.shared.dataTask(with: req) { data, _, err in
guard let data = data, err == nil else {
return
}
do {
let model = try JSONDecoder().decode(MyJsonModel.self, from: data)
DispatchQueue.main.async {
completion(model, nil)
}
} catch {
completion(nil, error)
}
}.resume()
}
}
虽然上面的例子可以编译和运行,但有几个错误。首先,我们没有在函数可以退出的地方使用完成处理程序。在同步编写代码时,也更难阅读。
为了改进上面的代码,我们将使用async 和await 。
在你的代码中使用async/await
当iOS 15和macOS 12在2021年秋季发布时,开发人员将能够使用新的async/await语法。你已经可以在JavaScript和C#等语言中使用async/await。
这两个关键词正在成为开发者在现代编程语言中编写并发代码的最佳实践。让我们来看看之前的函数goGrabSomething ,用新的async/await语法重写的。
swift
func goGrabSomething() async throws -> MyJsonModel? {
var model: MyJsonModel? = nil
let ourl = URL(string: "https://mydomain.com/api/v1/getsomejsondata")
if let url = ourl {
let req = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: req)
model = try JSONDecoder().decode(MyJsonModel.self, from: data)
}
return model
}
在上面的例子中,我们在throws 之前和函数名之后添加了async 关键字。如果我们的函数没有抛出,async 会在-> 之前。
我能够改变函数签名,使其不再需要完成。现在,我们可以返回已经从我们的API调用中解码的对象。
在我们的函数中,我在我的URLSession.shared.data(for: URLRequest) 前面使用关键字await 。由于URLSession 数据函数可以抛出一个错误,我在await 关键字前面加了一个try 。
每次我们在函数的主体中使用await ,都会产生一个延续。如果系统在处理我们的函数时需要等待,它可以暂停我们的函数,直到它准备从暂停状态返回。
如果我们试图从同步代码中调用goGrabSomething 函数,就会失败。Swift为这个用例提供了一个很好的变通方法我们可以在同步代码中使用一个async 闭包来调用我们的async 函数。
swift
async {
var myModel = try await goGrabSomething()
print("Name: \(myModel.name)")
}
现在,Swift有自己的系统来管理并发性和并行性。通过利用这些新的关键字,我们可以利用系统中新的并发功能。
最终的结果是,我们能够写出一个更容易阅读且包含更少代码的函数。
总结
Swift中的Async/await大大简化了我们在iOS应用程序中编写并发代码的方式。你可以通过下载Xcode 13并在iOS 15和macOS 12的测试版上运行这些例子来体验这些新功能。
这篇文章只是触及了这些新功能的表面。例如,Swift还增加了一个actor 对象类型,允许开发者创建包含共享mutable 状态的write 对象,这些对象可以跨线程使用而不存在竞赛条件。
我希望你喜欢这篇文章。如果你有兴趣进一步了解Swift中的async/await,请观看苹果的WWDC21演讲。
The postConcurrency in Swift:使用新的async/await语法》首次出现在LogRocket博客上。