簡介
為了解決傳統異步編程中回調block無限“套娃”的問題,蘋果官方于Swift5.5版本引入了新的異步編程理念
try await
,類似于同步的異步
(異步等待)方式,大大簡化了異步編程的代碼,提高了邏輯清晰性。
async throws
可用于下列標識:
- 屬性(計算屬性)
- 方法
- 初始化器
示例
異步下載一張圖片
enum ImageDownloadError: Error {
case failed
}
func downloadImage(url: String) async throws -> UIImage {
try Task.checkCancellation()
guard let aURL = URL(string: url) else {
throw URLError(.badURL)
}
let request = URLRequest(url: aURL)
print("Started downloading... \(url)")
let (data, _) = try await URLSession.shared.data(for: request)
guard data.count > 0, let image = UIImage(data: data) else {
throw ImageDownloadError.failed
}
print("Finished downloading")
return image
}
let task = Task {
do {
let idx = 1 + arc4random() % 20
let image = try await downloadImage(url: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution/\(idx).jpg")
print("Success: ", image)
} catch {
print("Failure: \(error.localizedDescription)")
}
}
// 0.1s后取消任務,這時候downloadImage異步任務直接會被thrown,
// 不會繼續執行session網絡請求
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
task.cancel()
}
- 如果不
Task.checkCancellation
的話,一旦URLSession
任務發出去,就會被執行完(即使外部task已經被cancel) - 如果進行了
Task.checkCancellation
,那么當URLSession
任務完成的時候,如果檢測到所在task已經被cancel了,則不會返回URLSession
的執行結果,而是直接throwcancelled
error - 此外,如果不想在Task被cancel的時候拋出異常,而是當成正常操作,也可以如下操作:
if Task.isCancelled {
// 自定義默認返回值
return someDefaultValue
}
或者,拋出自定義error:
if Task.isCancelled {
throw MyError.some
}
打印:
Started downloading...image download url
Failure: cancelled
- 異步串行任務
do {
var getOneImageUrl: String {
let idx = 1 + arc4random() % 20
return "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution/\(idx).jpg"
}
let image1 = try await downloadImage(url: getOneImageUrl)
print("Success image1: ", image1)
let image2 = try await downloadImage(url: getOneImageUrl)
print("Success image2: ", image2)
let image3 = try await downloadImage(url: getOneImageUrl)
print("Success image3: ", image3)
} catch {
print("Failure: \(error.localizedDescription)")
}
打?。?br> Started: 3.jpg
Finished: 3.jpg
Success image1: <UIImage:0x600003769200 anonymous {3688, 2459} renderingMode=automatic>
Started: 11.jpg
Finished: 11.jpg
Success image2: <UIImage:0x600003760a20 anonymous {3886, 2595} renderingMode=automatic>
Started: 1.jpg
Finished: 1.jpg
Success image3: <UIImage:0x600003764d80 anonymous {6000, 4000} renderingMode=automatic>
- 主線程方法
@MainActor
func showImage(_ image: UIImage) {
imageView.image = image
}
- 也可以使用
MainActor
的run
方法來包裹需要主線程執行的block
,類似于GCD main queue:
/*
public static func run<T>(
resultType: T.Type = T.self,
body: @MainActor @Sendable () throws -> T
) async rethrows -> T
*/
var img: UIImage?
...
// give the `img` to some specific value
...
// need to capture the mutable value into capture list or compiles error
MainActor.run{ [img] in
self.imageView.image = img
}
...
// or can also be replaced by this:
let img: UIImage
if xx {
img = someValue
} else {
img = nil
}
// and then, the `img` property will be immutable anymore,
// so it is can be used in `MainActor` context
MainActor.run{
self.imageView.image = img
}
@MainActor
標識表示在主線程調用該方法(自動切換到)
- 調用
let image1 = try await downloadImage(url: getOneImageUrl)
print("Success image1: ", image1)
await showImage(image1)
這里有兩個新概念:Task和MainActor,使用Task的原因是在同步線程和異步線程之間,我們需要一個橋接,我們需要告訴系統開辟一個異步環境,否則編譯器會報 'async' call in a function that does not support concurrency的錯誤。 另外Task表示開啟一個任務。@MainActor表示讓showImage方法在主線程執行。
使用 async-await并不會阻塞主線程,在同一個Task中,遇到await,后面的任務將會被掛起,等到await任務執行完后,會回到被掛起的地方繼續執行。這樣就做到了 異步串行。
異步并發 async-let
async let 和 let 類似,它定義一個本地常量,并通過等號右側的表達式來初始化這個常量。區別在于,這個初始化表達式必須是一個異步函數的調用,通過將這個異步函數“綁定”到常量值上,Swift 會創建一個并發執行的子任務,并在其中執行該異步函數。async let 賦值后,子任務會立即開始執行。如果想要獲取執行的結果 (也就是子任務的返回值),可以對賦值的常量使用 await 等待它的完成。
當 v0 任務完成后,它的結果將被暫存在它自身的續體棧上,等待執行上下文通過 await 切換到自己時,才會把結果返回。
如果沒有 await,那么 Swift 并發會在被綁定的常量離開作用域時,隱式地將綁定的子任務取消掉,然后進行 await。
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBAction func syncConcurrent(_ sender: UIControl) {
Task {
let image = try await downloadImageThumbnail(id: 1+arc4random()%20)
self.imageView.image = image
}
}
enum ThumbnailError: Error {
case badImage
}
func downloadImageThumbnail(id: UInt32) async throws -> UIImage {
try Task.checkCancellation()
async let image = downloadImage(id: id)
async let metadata = downloadImageMetadata(id: id)
guard let thumbnail = try await image.preparingThumbnail(of: try await metadata) else {
throw ThumbnailError.badImage
}
return thumbnail
}
func downloadImage(id: UInt32) async throws -> UIImage {
try Task.checkCancellation()
print("started download image...")
guard let aURL = URL(string: getOneImageUrl(id: id)) else {
throw URLError(.badURL)
}
let request = URLRequest(url: aURL)
let (data, _) = try await URLSession.shared.data(for: request)
guard let image = UIImage(data: data) else {
throw ThumbnailError.badImage
}
print("ended download image")
return image
}
func downloadImageMetadata(id: UInt32) async throws -> CGSize {
try Task.checkCancellation()
print("started download image metadata...")
let image = try await downloadImage(id: id)
let height: CGFloat = 200
let width = image.size.width/image.size.height * height
print("ended download image metadata")
return .init(width: width, height: height)
}
func getOneImageUrl(id: UInt32? = nil) -> String {
let idx = id ?? 1 + arc4random() % 20
return "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution/\(idx).jpg"
}
}
async let
相當于對已存在的某個異步任務(方法)進行了二次封裝,然后返回一個新的匿名異步任務,再將這個異步任務進行try await
待其執行完成,就可使用結果值了。
Group Tasks
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@frozen public struct ThrowingTaskGroup<ChildTaskResult, Failure> where Failure : Error {
...
}
- group 滿足 AsyncSequence,因此我們可以使用 for await 的語法來獲取子任務的執行結果。group 中的某個任務完成時,它的結果將被放到異步序列的緩沖區中。每當 group 的 next 會被調用時,如果緩沖區里有值,異步序列就將它作為下一個值給出;如果緩沖區為空,那么就等待下一個任務完成,這是異步序列的標準行為。
- for await 的結束意味著異步序列的 next 方法返回了 nil,此時group 中的子任務已經全部執行完畢了,withTaskGroup 的閉包也來到最后。接下來,外層的 “End” 也會被輸出。整個結構化并發結束執行。
- 即使我們沒有明確 await 任務組,編譯器在檢測到結構化并發作用域結束時,會為我們自動添加上 await 并在等待所有任務結束后再繼續控制流:
for i in 0 ..< 3 {
group.addTask {
await work(i)
}
}
// 編譯器自動生成的代碼
for await _ in group { }
即使手動退出某個子任務的await行為,編譯器也會自動加上如下的隱式操作:
for await result in group {
print("Get result: \(result)")
// 在首個子任務完成后就跳出
break
}
print("Task ended")
// 編譯器自動生成的代碼
await group.waitForAll()
public mutating func addTask(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> ChildTaskResult)
注意addTask的operation是一個返回值類型為
ChildTaskResult
的@Sendable
的block,這意味在多個異步的task之間可以進行數據send達到線程通信的目的,也保證了數據訪問的線程安全-
也可以使用
addTaskUnlessCancelled() -> Bool
這個方法,如果group外層的task被cancel了,則不會再addTask了:- Adds a child task to the group, unless the group has been canceled.
- This method doesn't throw an error, even if the child task does.
group.cancelAll()
取消全部任務
func fetchThumbnails(ids: [UInt32]) async throws -> [UInt32: UIImage] {
var result: [UInt32: UIImage] = [:]
try await withThrowingTaskGroup(of: (UInt32, UIImage).self) { group in
for id in ids {
group.addTask(priority: .medium) { [self] in
return (id, try await downloadImageThumbnail(id: id))
}
}
for try await (id, thumbnail) in group {
result[id] = thumbnail
}
}
return result
}
- 調用
@IBAction func btnTapped(_ sender: UIControl) {
Task {
let images = try await fetchThumbnails(ids: [1, 3, 5, 7])
print("All thumbnail images downloaded")
for (id,img) in images {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(id), qos: .userInteractive) {
self.imageView.image = img
}
}
}
}
Unstructured Tasks
如果將非結構化的異步方法調用和結構化的異步任務結合起來,可以利用Task{}包裹,并且將其存儲,在合適的時機進行cancel和置nil
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
display(thumbnails, in: cell)
}
}
func collectionView(_ view: UICollectionView, didEndDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
thumbnailTasks[item]?.cancel()
}
}
Unstructured Detached Tasks
任務嵌套的異步子任務,可以通過group進行組合使其并發執行
@MainActor
class MyDelegate: UICollectionViewDelegate {
var thumbnailTasks: [IndexPath: Task<Void, Never>] = [:]
func collectionView(_ view: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt item: IndexPath) {
let ids = getThumbnailIDs(for: item)
thumbnailTasks[item] = Task {
defer { thumbnailTasks[item] = nil }
let thumbnails = await fetchThumbnails(for: ids)
Task.detached(priority: .background) {
withTaskGroup(of: Void.self) { g in
g.async { writeToLocalCache(thumbnails) }
g.async { log(thumbnails) }
g.async { ... }
}
}
display(thumbnails, in: cell)
}
}
}
異步計算屬性
var asyncCover: UIImage? {
get async {
return await getRemoteCoverImage()
}
}
func getRemoteCoverImage() async -> UIImage? {
//do some network requests
return nil
}
異步函數
在函數聲明的返回箭頭前面,加上 async 關鍵字,就可以把一個函數聲明為異步函數:
func loadSignature() async throws -> String {
fatalError("暫未實現")
}
異步函數的 async 關鍵字會幫助編譯器確保兩件事情:
- 它允許我們在函數體內部使用 await 關鍵字;
- 它要求其他人在調用這個函數時,使用 await 關鍵字。
這和與它處于類似位置的 throws 關鍵字有點相似。在使用 throws 時,它允許我們在函數內部使用 throw 拋出錯誤,并要求調用者使用 try 來處理可能的拋出。
結構化并發
- 基于 Task 的結構化并發模型
在 Swift 并發編程中,結構化并發需要依賴異步函數,而異步函數又必須運行在某個任務上下文中,因此可以說,想要進行結構化并發,必須具有任務上下文。實際上,Swift 結構化并發就是以任務為基本要素進行組織的。
- 當前任務狀態
Swift 并發編程把異步操作抽象為任務,在任意的異步函數中,我們總可是使用 withUnsafeCurrentTask 來獲取和檢查當前任務:
func foo() async {
withUnsafeCurrentTask { task in
// 3
if let task = task {
// 4
print("Cancelled: \(task.isCancelled)")
// => Cancelled: false
print(task.priority)
// TaskPriority(rawValue: 33)
} else {
print("No task")
}
}
}
actor模型
解決多線程數據訪問安全問題,類似于lock的作用,保證了數據的安全訪問
actor Holder {
var results: [String] = []
func setResults(_ results: [String]) {
self.results = results
}
func append(_ value: String) {
results.append(value)
}
}
actor 內部會提供一個隔離域:在 actor 內部對自身存儲屬性或其他方法的訪問,比如在 append(_:) 函數中使用 results 時,可以不加任何限制,這些代碼都會被自動隔離在被封裝的“私有隊列”里。但是從外部對 actor 的成員進行訪問時,編譯器會要求切換到 actor 的隔離域,以確保數據安全。在這個要求發生時,當前執行的程序可能會發生暫停。編譯器將自動把要跨隔離域的函數轉換為異步函數,并要求我們使用 await 來進行調用。
- 調用:由于是以類似異步隊列-線程的方式進行了內部封裝/隔離,所以訪問這些數據需要使用
await
標識,表示線程的調度
// holder.setResults([])
await holder.setResults([])
// holder.append(data.appending(signature))
await holder.append(data.appending(signature))
// print("Done: \(holder.getResults())")
print("Done: \(await holder.results)")
當然,這種數據隔離只解決同時訪問的造成的內存問題 (在 Swift 中,這種不安全行為大多數情況下表現為程序崩潰),并不會解決多個異步讓數據增加/減少導致數據錯亂不同步問題。
我們可以使用 @MainActor
來確保 UI 線程的隔離。
如果你是在一個沒有“完全遷移”到 Swift Concurrency Safe 的項目的話,可能需要在 class 申明上也加上 @MainActor 來讓它生效。
另外,需要指出的是,@MainActor 需要 async 環境來完成 actor 的切換。
Group抽象封裝(簡化)
封裝一個
MyAsyncTaskGroup
泛型化的group
class MyAsyncTaskGroup<Data, ChildTaskResult> {
typealias Operation = (Data) async throws -> (ChildTaskResult)
let datas: [Data]
let operation: Operation
init(children datas: [Data], child operation: @escaping Operation) {
self.datas = datas
self.operation = operation
}
func throwingStart() async throws -> [ChildTaskResult] {
var results: [ChildTaskResult] = []
try await withThrowingTaskGroup(of: ChildTaskResult.self) { group in
for data in datas {
group.addTask{ [self] in
try await self.operation(data)
}
}
for try await result in group {
results.append(result)
}
}
return results
}
}
- 自定義具體操作的group
class AsyncImageDownloadGroup: MyAsyncTaskGroup<URL, UIImage> {
let URLs: [URL]
init(urls: [String]) {
self.URLs = urls.compactMap{ .init(string: $0) }
super.init(children: self.URLs) {
try await API.shared.downloadImage(withURL: $0)
}
}
}
- 調用
@IBAction func syncConcurrent(_ sender: UIControl) {
Task {
let images = try await AsyncImageDownloadGroup(
urls: [
API.shared.getOneImageUrl(),
API.shared.getOneImageUrl(),
API.shared.getOneImageUrl(),
API.shared.getOneImageUrl()
]
).throwingStart()
print("All thumbnail images downloaded")
for (idx,img) in images.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(idx), qos: .userInteractive) {
self.imageView.image = img
}
}
}
}
也可以這樣使用:
Task {
let images = try await MyAsyncTaskGroup(
childrenDatas: [
API.shared.getOneImageUrl(),
API.shared.getOneImageUrl(),
API.shared.getOneImageUrl(),
API.shared.getOneImageUrl()
].compactMap{ URL(string: $0) }, childOperation: {
try await API.shared.downloadImage(withURL: $0)
}
).throwingStart()
print("All thumbnail images downloaded")
for (idx,img) in images.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(idx), qos: .userInteractive) {
self.imageView.image = img
}
}
}
這樣一封裝,是不是感覺比系統原生的簡潔多了。
Actor Reentrancy
核心代碼
do {
let image = try await task.value
cache[url] = .ready(image)
return image
} catch {
cache[url] = nil
throw error
}
- 此處執行
reentrancy
操作(等待task完成),目的是當downloadImage完成時, - 立即將對應的image進行緩存,并返回給外部調用者
- 如果下載失敗,則將對應的task存儲置為nil
actor ImageDownloader {
private enum CacheEntry {
case inProgress(Task<Image, Error>)
case ready(Image)
}
private var cache: [URL: CacheEntry] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
switch cached {
case .ready(let image):
return image
case .inProgress(let task):
return try await task.value
}
}
let task = Task {
try await downloadImage(from: url)
}
cache[url] = .inProgress(task)
do {
let image = try await task.value
cache[url] = .ready(image)
return image
} catch {
cache[url] = nil
throw error
}
}
}
Actor Isolate
actor LibraryAccount {
let idNumber: Int
var booksOnLoan: [Book] = []
}
extension LibraryAccount: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(idNumber)
}
}
Sendable & @Sendable
Sendable
是一個協議,它標識的數據模型實例可以在actor的環境中被安全的訪問
struct Book: Sendable {
var title: String
var authors: [Author]
}
Sendable
協議下的模型內部也要求所有自定義類型均實現Sendable
協議,否則就會編譯報錯,但是我們可以實現一個類似于包裹器的泛型結構體Pair
,讓其實現Sendable
協議,就可以了:
struct Pair<T, U> {
var first: T
var second: U
}
extension Pair: Sendable where T: Sendable, U: Sendable {
}
@sendable
可以標識一個func或closure的類型,表示自動實現Sendable
協議
public mutating func addTask(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async throws -> ChildTaskResult
)
@MainActor
該標識的方法,將會在主線程執行,但同樣的也要在調用的地方用
await
標識
@MainActor func checkedOut(_ booksOnLoan: [Book]) {
booksView.checkedOutBooks = booksOnLoan
}
// Swift ensures that this code is always run on the main thread.
await checkedOut(booksOnLoan)
同樣的,自定義類型也可以用@MainActor
標識,表示其中的屬性、方法等均在主線程執行,常用于UI類的標識。當然,如果用nonisolated
標識某個方法、屬性,表示其可以脫離于當前類型的main thread的context。
@MainActor class MyViewController: UIViewController {
func onPress(...) { ... } // implicitly @MainActor
// 這個方法可以脫離主線程運行
nonisolated func fetchLatestAndDisplay() async { ... }
}
Async Sequence
@main
struct QuakesTool {
static func main() async throws {
let endpointURL = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv")!
// skip the header line and iterate each one
// to extract the magnitude, time, latitude and longitude
for try await event in endpointURL.lines.dropFirst() {
let values = event.split(separator: ",")
let time = values[0]
let latitude = values[1]
let longitude = values[2]
let magnitude = values[4]
print("Magnitude \(magnitude) on \(time) at \(latitude) \(longitude)")
}
}
}
上面的for-await-in
類似于:
var iterator = quakes.makeAsyncIterator()
while let quake = await iterator.next() {
if quake.magnitude > 3 {
displaySignificantEarthquake(quake)
}
}
上述for-try await-in
可以正常配合break
或continue
、do{} catch{}
使用
Bytes from a FileHandle
也可以用于以異步序列的方式讀取本地/網絡文件:
let url = URL(fileURLWithPath: "/tmp/somefile.txt")
for try await line in url.lines {
...
}
Bytes from a URLSession
let (bytes, response) = try await URLSession.shared.bytes(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 /* OK */
else {
throw MyNetworkingError.invalidServerResponse
}
for try await byte in bytes {
...
}
Async Notifications
let center = NotificationCenter.default
let notification = await center.notifications(named: .NSPersistentStoreRemoteChange).first {
$0.userInfo[NSStoreUUIDKey] == storeUUID
}
Custom AsyncSequence
class QuakeMonitor {
var quakeHandler: (Quake) -> Void
func startMonitoring()
func stopMonitoring()
}
let quakes = AsyncStream(Quake.self) { continuation in
let monitor = QuakeMonitor()
monitor.quakeHandler = { quake in
continuation.yield(quake)
}
continuation.onTermination = { @Sendable _ in
monitor.stopMonitoring()
}
monitor.startMonitoring()
}
let significantQuakes = quakes.filter { quake in
quake.magnitude > 3
}
for await quake in significantQuakes {
...
}
對應的,也可以使用AsyncThrowingStream
包裹對應的Sequence
數據流。
Continuation
由于
async/throws
是需要await
然后立即return
對應的結果的,那么如果在一個新的async
方法里,想要嵌套原有的基于handler
的異步方法,那么就沒法return
了,因為在handler
里邊才能進行結果的對錯校驗。為了搭配這2種方式,引入了Continuation
來異步轉發
handler返回的數據,然后return
。
問題:
func asyncRequest<T>(_ req: URLRequest) async throws -> T {
Network.request(req) { (error, data) in
if let error = error {
throw error
}
else if let data = data {
// so how to `return` the data?
}
}
}
struct Network {
static func request(_ req: URLRequest, completion: @escaping (Error?, Data?)->Void) {
...
}
}
解決:
func withCheckedThrowingContinuation<T>(
function: String = #function,
_ body: (CheckedContinuation<T, Error>) -> Void
) async throws -> T
// resume
public func resume(returning x: T)
public func resume(throwing x: E)
CheckedContinuation<T, Error>
func asyncRequest<T: Decodable>(_ req: URLRequest) async throws -> T {
typealias RequestContinuation = CheckedContinuation<T, Error>
return try await withCheckedThrowingContinuation{ (continuation: RequestContinuation) in
Network.request(req) { (error, data: T?) -> Void in
if let error = error {
continuation.resume(throwing: error)
}
else if let data = data {
continuation.resume(returning: data)
}
}
}
}
struct Network {
enum Error: Swift.Error {
case noData
}
static func request<T: Decodable>(
_ req: URLRequest,
completion: @escaping (Swift.Error?, T?)->Void
) {
let handler: (Foundation.Data?, URLResponse?, Swift.Error?) -> Void = { data,_,error in
if let error = error {
return completion(error, nil)
}
guard let data = data else {
return completion(Error.noData, nil)
}
do {
let object = try JSONDecoder().decode(T.self, from: data)
return completion(nil, object)
}
catch {
return completion(error, nil)
}
}
let task = URLSession.shared.dataTask(with: req, completionHandler: handler)
task.resume()
}
}
與handler
回調類似的,還有基于delegate
的方式,同樣可以使用Continuation
來異步轉發:
class ViewController: UIViewController {
private var activeContinuation: CheckedContinuation<[Post], Error>?
func sharedPostsFromPeer() async throws -> [Post] {
try await withCheckedThrowingContinuation { continuation in
self.activeContinuation = continuation
self.peerManager.syncSharedPosts()
}
}
}
extension ViewController: PeerSyncDelegate {
func peerManager(_ manager: PeerManager, received posts: [Post]) {
self.activeContinuation?.resume(returning: posts)
// guard against multiple calls to resume
self.activeContinuation = nil
}
func peerManager(_ manager: PeerManager, hadError error: Error) {
self.activeContinuation?.resume(throwing: error)
// guard against multiple calls to resume
self.activeContinuation = nil
}
}