Swift 5.5 | Async throws/ Try await

簡介

為了解決傳統(tǒng)異步編程中回調(diào)block無限“套娃”的問題崇堰,蘋果官方于Swift5.5版本引入了新的異步編程理念try await坦喘,類似于同步的異步(異步等待)方式贞让,大大簡化了異步編程的代碼财搁,提高了邏輯清晰性个束。

async throws可用于下列標(biāo)識(shí):

  • 屬性(計(jì)算屬性)
  • 方法
  • 初始化器

示例

異步下載一張圖片

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后取消任務(wù)慕购,這時(shí)候downloadImage異步任務(wù)直接會(huì)被thrown,
// 不會(huì)繼續(xù)執(zhí)行session網(wǎng)絡(luò)請(qǐng)求
DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
    task.cancel()
}
  • 如果不Task.checkCancellation的話茬底,一旦URLSession任務(wù)發(fā)出去,就會(huì)被執(zhí)行完(即使外部task已經(jīng)被cancel)
  • 如果進(jìn)行了Task.checkCancellation获洲,那么當(dāng)URLSession任務(wù)完成的時(shí)候阱表,如果檢測到所在task已經(jīng)被cancel了,則不會(huì)返回URLSession的執(zhí)行結(jié)果贡珊,而是直接throw cancelled error
  • 此外最爬,如果不想在Task被cancel的時(shí)候拋出異常,而是當(dāng)成正常操作门岔,也可以如下操作:
if Task.isCancelled {
// 自定義默認(rèn)返回值
  return someDefaultValue
}

或者爱致,拋出自定義error:

if Task.isCancelled {
  throw MyError.some
}

打印:
Started downloading... image download url
Failure: cancelled

  • 異步串行任務(wù)
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)")
    }

打印:
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
}
  • 也可以使用MainActorrun方法來包裹需要主線程執(zhí)行的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標(biāo)識(shí)表示在主線程調(diào)用該方法(自動(dòng)切換到)

  • 調(diào)用
let image1 = try await downloadImage(url: getOneImageUrl)
print("Success image1: ", image1)
await showImage(image1)

這里有兩個(gè)新概念:Task和MainActor糠悯,使用Task的原因是在同步線程和異步線程之間,我們需要一個(gè)橋接妻往,我們需要告訴系統(tǒng)開辟一個(gè)異步環(huán)境互艾,否則編譯器會(huì)報(bào) 'async' call in a function that does not support concurrency的錯(cuò)誤。 另外Task表示開啟一個(gè)任務(wù)讯泣。@MainActor表示讓showImage方法在主線程執(zhí)行纫普。

使用 async-await并不會(huì)阻塞主線程,在同一個(gè)Task中好渠,遇到await昨稼,后面的任務(wù)將會(huì)被掛起,等到await任務(wù)執(zhí)行完后拳锚,會(huì)回到被掛起的地方繼續(xù)執(zhí)行假栓。這樣就做到了 異步串行。

異步并發(fā) async-let

async let 和 let 類似晌畅,它定義一個(gè)本地常量但指,并通過等號(hào)右側(cè)的表達(dá)式來初始化這個(gè)常量。區(qū)別在于,這個(gè)初始化表達(dá)式必須是一個(gè)異步函數(shù)的調(diào)用棋凳,通過將這個(gè)異步函數(shù)“綁定”到常量值上拦坠,Swift 會(huì)創(chuàng)建一個(gè)并發(fā)執(zhí)行的子任務(wù),并在其中執(zhí)行該異步函數(shù)剩岳。async let 賦值后贞滨,子任務(wù)會(huì)立即開始執(zhí)行。如果想要獲取執(zhí)行的結(jié)果 (也就是子任務(wù)的返回值)拍棕,可以對(duì)賦值的常量使用 await 等待它的完成晓铆。

當(dāng) v0 任務(wù)完成后,它的結(jié)果將被暫存在它自身的續(xù)體棧上绰播,等待執(zhí)行上下文通過 await 切換到自己時(shí)骄噪,才會(huì)把結(jié)果返回。

如果沒有 await蠢箩,那么 Swift 并發(fā)會(huì)在被綁定的常量離開作用域時(shí)链蕊,隱式地將綁定的子任務(wù)取消掉,然后進(jìn)行 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相當(dāng)于對(duì)已存在的某個(gè)異步任務(wù)(方法)進(jìn)行了二次封裝滔韵,然后返回一個(gè)新的匿名異步任務(wù),再將這個(gè)異步任務(wù)進(jìn)行try await待其執(zhí)行完成掌实,就可使用結(jié)果值了陪蜻。

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 的語法來獲取子任務(wù)的執(zhí)行結(jié)果贱鼻。group 中的某個(gè)任務(wù)完成時(shí)宴卖,它的結(jié)果將被放到異步序列的緩沖區(qū)中。每當(dāng) group 的 next 會(huì)被調(diào)用時(shí)忱嘹,如果緩沖區(qū)里有值嘱腥,異步序列就將它作為下一個(gè)值給出;如果緩沖區(qū)為空拘悦,那么就等待下一個(gè)任務(wù)完成齿兔,這是異步序列的標(biāo)準(zhǔn)行為。
  • for await 的結(jié)束意味著異步序列的 next 方法返回了 nil础米,此時(shí)group 中的子任務(wù)已經(jīng)全部執(zhí)行完畢了分苇,withTaskGroup 的閉包也來到最后。接下來屁桑,外層的 “End” 也會(huì)被輸出医寿。整個(gè)結(jié)構(gòu)化并發(fā)結(jié)束執(zhí)行。
  • 即使我們沒有明確 await 任務(wù)組蘑斧,編譯器在檢測到結(jié)構(gòu)化并發(fā)作用域結(jié)束時(shí)靖秩,會(huì)為我們自動(dòng)添加上 await 并在等待所有任務(wù)結(jié)束后再繼續(xù)控制流:
for i in 0 ..< 3 {
    group.addTask {
      await work(i)
    }
  }

// 編譯器自動(dòng)生成的代碼
  for await _ in group { }

即使手動(dòng)退出某個(gè)子任務(wù)的await行為须眷,編譯器也會(huì)自動(dòng)加上如下的隱式操作:

for await result in group {
    print("Get result: \(result)")
    // 在首個(gè)子任務(wù)完成后就跳出
    break
  }
  print("Task ended")

  // 編譯器自動(dòng)生成的代碼
  await group.waitForAll()

public mutating func addTask(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> ChildTaskResult)

  • 注意addTask的operation是一個(gè)返回值類型為ChildTaskResult@Sendable的block,這意味在多個(gè)異步的task之間可以進(jìn)行數(shù)據(jù)send達(dá)到線程通信的目的沟突,也保證了數(shù)據(jù)訪問的線程安全

  • 也可以使用addTaskUnlessCancelled() -> Bool這個(gè)方法花颗,如果group外層的task被cancel了,則不會(huì)再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()取消全部任務(wù)

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
    }
  • 調(diào)用
@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

如果將非結(jié)構(gòu)化的異步方法調(diào)用和結(jié)構(gòu)化的異步任務(wù)結(jié)合起來惠拭,可以利用Task{}包裹扩劝,并且將其存儲(chǔ),在合適的時(shí)機(jī)進(jìn)行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

任務(wù)嵌套的異步子任務(wù)职辅,可以通過group進(jìn)行組合使其并發(fā)執(zhí)行

@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)
        }
    }
}

異步計(jì)算屬性

  var asyncCover: UIImage? {
        get async {
            return await getRemoteCoverImage()
        }
    }
    func getRemoteCoverImage() async -> UIImage? {
        //do some network requests
        return nil
    }

異步函數(shù)

在函數(shù)聲明的返回箭頭前面棒呛,加上 async 關(guān)鍵字,就可以把一個(gè)函數(shù)聲明為異步函數(shù):

func loadSignature() async throws -> String {
  fatalError("暫未實(shí)現(xiàn)")
}

異步函數(shù)的 async 關(guān)鍵字會(huì)幫助編譯器確保兩件事情:

  • 它允許我們?cè)诤瘮?shù)體內(nèi)部使用 await 關(guān)鍵字域携;
  • 它要求其他人在調(diào)用這個(gè)函數(shù)時(shí)簇秒,使用 await 關(guān)鍵字。

這和與它處于類似位置的 throws 關(guān)鍵字有點(diǎn)相似涵亏。在使用 throws 時(shí)宰睡,它允許我們?cè)诤瘮?shù)內(nèi)部使用 throw 拋出錯(cuò)誤,并要求調(diào)用者使用 try 來處理可能的拋出气筋。

結(jié)構(gòu)化并發(fā)

  • 基于 Task 的結(jié)構(gòu)化并發(fā)模型

在 Swift 并發(fā)編程中,結(jié)構(gòu)化并發(fā)需要依賴異步函數(shù)旋圆,而異步函數(shù)又必須運(yùn)行在某個(gè)任務(wù)上下文中宠默,因此可以說,想要進(jìn)行結(jié)構(gòu)化并發(fā)灵巧,必須具有任務(wù)上下文搀矫。實(shí)際上,Swift 結(jié)構(gòu)化并發(fā)就是以任務(wù)為基本要素進(jìn)行組織的刻肄。

  • 當(dāng)前任務(wù)狀態(tài)
    Swift 并發(fā)編程把異步操作抽象為任務(wù)瓤球,在任意的異步函數(shù)中,我們總可是使用 withUnsafeCurrentTask 來獲取和檢查當(dāng)前任務(wù):
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模型

解決多線程數(shù)據(jù)訪問安全問題敏弃,類似于lock的作用卦羡,保證了數(shù)據(jù)的安全訪問

actor Holder {
  var results: [String] = []
  func setResults(_ results: [String]) {
    self.results = results
  }
    
  func append(_ value: String) {
    results.append(value)
  }
}

actor 內(nèi)部會(huì)提供一個(gè)隔離域:在 actor 內(nèi)部對(duì)自身存儲(chǔ)屬性或其他方法的訪問,比如在 append(_:) 函數(shù)中使用 results 時(shí)麦到,可以不加任何限制绿饵,這些代碼都會(huì)被自動(dòng)隔離在被封裝的“私有隊(duì)列”里。但是從外部對(duì) actor 的成員進(jìn)行訪問時(shí)瓶颠,編譯器會(huì)要求切換到 actor 的隔離域拟赊,以確保數(shù)據(jù)安全。在這個(gè)要求發(fā)生時(shí)粹淋,當(dāng)前執(zhí)行的程序可能會(huì)發(fā)生暫停吸祟。編譯器將自動(dòng)把要跨隔離域的函數(shù)轉(zhuǎn)換為異步函數(shù)瑟慈,并要求我們使用 await 來進(jìn)行調(diào)用。

  • 調(diào)用:由于是以類似異步隊(duì)列-線程的方式進(jìn)行了內(nèi)部封裝/隔離屋匕,所以訪問這些數(shù)據(jù)需要使用await標(biāo)識(shí)葛碧,表示線程的調(diào)度
// 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)")

當(dāng)然,這種數(shù)據(jù)隔離只解決同時(shí)訪問的造成的內(nèi)存問題 (在 Swift 中炒瘟,這種不安全行為大多數(shù)情況下表現(xiàn)為程序崩潰)吹埠,并不會(huì)解決多個(gè)異步讓數(shù)據(jù)增加/減少導(dǎo)致數(shù)據(jù)錯(cuò)亂不同步問題。

我們可以使用 @MainActor 來確保 UI 線程的隔離疮装。

如果你是在一個(gè)沒有“完全遷移”到 Swift Concurrency Safe 的項(xiàng)目的話缘琅,可能需要在 class 申明上也加上 @MainActor 來讓它生效。

另外廓推,需要指出的是刷袍,@MainActor 需要 async 環(huán)境來完成 actor 的切換。

Group抽象封裝(簡化)

封裝一個(gè)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)
        }
    }
}
  • 調(diào)用
@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
                }
            }
        }

這樣一封裝樊展,是不是感覺比系統(tǒng)原生的簡潔多了呻纹。

Actor Reentrancy

核心代碼

do {
            let image = try await task.value
            cache[url] = .ready(image)
            return image
        } catch {
            cache[url] = nil
            throw error
        }
  • 此處執(zhí)行reentrancy操作(等待task完成),目的是當(dāng)downloadImage完成時(shí)专缠,
  • 立即將對(duì)應(yīng)的image進(jìn)行緩存雷酪,并返回給外部調(diào)用者
  • 如果下載失敗,則將對(duì)應(yīng)的task存儲(chǔ)置為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是一個(gè)協(xié)議涝婉,它標(biāo)識(shí)的數(shù)據(jù)模型實(shí)例可以在actor的環(huán)境中被安全的訪問

struct Book: Sendable {
    var title: String
    var authors: [Author]
}

Sendable協(xié)議下的模型內(nèi)部也要求所有自定義類型均實(shí)現(xiàn)Sendable協(xié)議哥力,否則就會(huì)編譯報(bào)錯(cuò),但是我們可以實(shí)現(xiàn)一個(gè)類似于包裹器的泛型結(jié)構(gòu)體Pair墩弯,讓其實(shí)現(xiàn)Sendable協(xié)議吩跋,就可以了:

struct Pair<T, U> {
    var first: T
    var second: U
}

extension Pair: Sendable where T: Sendable, U: Sendable {
}

@sendable可以標(biāo)識(shí)一個(gè)func或closure的類型,表示自動(dòng)實(shí)現(xiàn)Sendable協(xié)議

public mutating func addTask(
  priority: TaskPriority? = nil, 
  operation: @escaping @Sendable () async throws -> ChildTaskResult
)

@MainActor

該標(biāo)識(shí)的方法渔工,將會(huì)在主線程執(zhí)行锌钮,但同樣的也要在調(diào)用的地方用await標(biāo)識(shí)

@MainActor func checkedOut(_ booksOnLoan: [Book]) {
    booksView.checkedOutBooks = booksOnLoan
}

// Swift ensures that this code is always run on the main thread.
await checkedOut(booksOnLoan)

同樣的,自定義類型也可以用@MainActor標(biāo)識(shí)引矩,表示其中的屬性梁丘、方法等均在主線程執(zhí)行,常用于UI類的標(biāo)識(shí)脓魏。當(dāng)然兰吟,如果用nonisolated標(biāo)識(shí)某個(gè)方法、屬性茂翔,表示其可以脫離于當(dāng)前類型的main thread的context混蔼。

@MainActor class MyViewController: UIViewController {
    func onPress(...) { ... } // implicitly @MainActor

// 這個(gè)方法可以脫離主線程運(yùn)行
    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可以正常配合breakcontinuedo{} catch{}使用

Bytes from a FileHandle

也可以用于以異步序列的方式讀取本地/網(wǎng)絡(luò)文件:

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 {
    ...
}

對(duì)應(yīng)的珊燎,也可以使用AsyncThrowingStream包裹對(duì)應(yīng)的Sequence數(shù)據(jù)流惭嚣。

Continuation

由于async/throws是需要await然后立即return對(duì)應(yīng)的結(jié)果的遵湖,那么如果在一個(gè)新的async方法里,想要嵌套原有的基于handler的異步方法晚吞,那么就沒法return了延旧,因?yàn)樵?code>handler里邊才能進(jìn)行結(jié)果的對(duì)錯(cuò)校驗(yàn)。為了搭配這2種方式槽地,引入了Continuation異步轉(zhuǎn)發(fā)handler返回的數(shù)據(jù)迁沫,然后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回調(diào)類似的捌蚊,還有基于delegate的方式集畅,同樣可以使用Continuation來異步轉(zhuǎn)發(fā):

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
    }
}

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市缅糟,隨后出現(xiàn)的幾起案子挺智,更是在濱河造成了極大的恐慌,老刑警劉巖窗宦,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赦颇,死亡現(xiàn)場離奇詭異,居然都是意外死亡赴涵,警方通過查閱死者的電腦和手機(jī)媒怯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來髓窜,“玉大人沪摄,你說我怎么就攤上這事∩春妫” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵祈餐,是天一觀的道長擂啥。 經(jīng)常有香客問我,道長帆阳,這世上最難降的妖魔是什么哺壶? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蜒谤,結(jié)果婚禮上山宾,老公的妹妹穿的比我還像新娘。我一直安慰自己鳍徽,他們只是感情好资锰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阶祭,像睡著了一般绷杜。 火紅的嫁衣襯著肌膚如雪直秆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天鞭盟,我揣著相機(jī)與錄音圾结,去河邊找鬼。 笑死齿诉,一個(gè)胖子當(dāng)著我的面吹牛筝野,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粤剧,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼歇竟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了俊扳?” 一聲冷哼從身側(cè)響起途蒋,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎馋记,沒想到半個(gè)月后号坡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡梯醒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年宽堆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茸习。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡畜隶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出号胚,到底是詐尸還是另有隱情籽慢,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布猫胁,位于F島的核電站箱亿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏弃秆。R本人自食惡果不足惜届惋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望菠赚。 院中可真熱鬧脑豹,春花似錦、人聲如沸衡查。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽峡捡。三九已至击碗,卻和暖如春筑悴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背稍途。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工阁吝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人械拍。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓突勇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親坷虑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子甲馋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容