Async and Await in Swift 5.5

前言

老司机技术周报与字节音乐联合主办了今年的 T 沙龙上海专场。本次沙龙邀请了 4 位国内嘉宾,特邀了 2 位国外嘉宾。彭玉堂受邀为大家分享【Async and Await in Swift 5.5】,何星基于这次分享视频为大家整理此文,辛苦二位!阅读原文,领取 PPT。

讲师简介:彭玉堂,阿里巴巴淘宝技术基础平台部无线开发专家,2009 年毕业于北京航空航天大学信息与计算科学专业,2013 年加入阿里巴巴,参与了虾米音乐、手机天猫、手机淘宝几个移动客户端的架构和性能保障等相关工作。

编辑简介:何星,字节跳动音乐团队工程师

异步编程的现状

在日常开发中,我们一般通过回调的方式编写异步代码,像是这样:

func processImageData(completionBlock: (_ result: Image?, _ error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            completionBlock(nil, error)
            return
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                completionBlock(nil, error)
                return
            }
            decodeImage(dataResource, imageResource) { imageTmp, error in
                guard let imageTmp = imageTmp else {
                    completionBlock(nil, error)
                    return
                }
                dewarpAndCleanupImage(imageTmp) { imageResult, error in
                    guard let imageResult = imageResult else {
                        completionBlock(nil, error)
                        return
                    }
                    completionBlock(imageResult)
                }
            }
        }
    }
}

这种方式虽然很常见,但仍有很多明显的缺陷:

  1. 回调地狱:回调嵌套多层,代码可读性差
  2. 错误处理复杂:每个分支都有可能发生错误,需要回调错误,非常复杂
  3. 嵌套多层后,条件判断变得复杂,且容易出错
  4. 容易犯低级错误 (忘记回调或者回调后忘记返回等)

Swift 中异步编程如此繁琐,那其他语言是如何处理的呢?

Kotlin 中的异步编程方式

Kotlin 中不使用协程的话,异步代码与 Swift 类似:

fun postItem(item: Item) {
    preparePostAsync { token ->
        submitPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

fun preparePostAsync(callback: (Token) -> Unit) {
    // 发起请求并立即返回
    // 设置稍后调用的回调
}

使用协程也十分简单,通过 launch 接口,直接调用 async 方法:

fun postItem(item: Item) {
   launch {
       val token = preparePost()
       val post = submitPost(token, item)
       processPost(post)
  }
}

suspend fun preparePost(): Token {
   // 发起请求并挂起该协程
   return suspendCoroutine { /* ... */ }
}

Node.js 中的异步编程
Node.js 中可以使用 Promise/Callback 实现回调:
function requestWithRetry(url, retryCount) {
    if (retryCount) {
        return new Promise((resolve, reject) => {
            const timeout = Math.pow(2, retryCount);
            
            setTimeout(() => {
                console.log("Waiting", timeout, "ms");
                _requestWithRetry(url, retryCount)
                .then(resolve)
                .catch(reject);
            }, timeout);
        });
    } else {
        return _requestWithRetry(url, 0);
    }
}

function _requestWithRetry(url, retryCount) {
    return request(url, retryCount)
    .catch((err) => {
        if (err.statusCode && err.statusCode >= 500) {
            console.log("Retrying", err.message, retryCount);
            return requestWithRetry(url, ++retryCount);
        }
        throw err;
    });
}

使用协程异步编程,大大简化了代码:

async function requestWithRetry(url) {
    const MAX_RETRIES = 10;
    for (let i = 0; i <= MAX_RETRIES; i++) {
        try {
            return await request(url);
        } catch (err) {
            const timeout = Math.pow(2, i);
            console.log("Waiting", timeout, "ms");
            await wait(timeout);
            console.log("Retrying", err.message, i);
        }
    }
}

可以看到,协程不仅打开了异步编程的大门,还提供了大量其他的可能性。

什么是协程

协程是一种在非抢占式多任务场景下生成可以在特定位置挂起恢复执行入口的程序组件。

Async and Await in Swift 5.5

上面的定义有些复杂,具体是什么意思呢?通常,我们的代码都是顺序执行的,执行过程中不会中断。协程的不同之处在于它支持了执行过程中的挂起和恢复。例如在 A 方法执行的期间,调用 B 方法,这时 A 方法在会被挂起,但并没有返回。在  B 方法里面,执行一段时候,它又可以恢复回来 A 方法。

协程的特性

非抢占式:

  • 无需系统调用

作为对比,多线程执行一般需要通过系统调用去分配线程进行执行

  • 协程是线程安全的,无需锁

挂起执行时:

  • 保存寄存器和栈
  • 不影响其他协程执行

恢复执行:

  • 恢复之前的寄存器和栈
  • 无缝切换回之前的执行逻辑

Swift Async/await

Swift 5.5 为我们带来了协程模型 Async/Await,可以通过顺序书写的方式编写异步代码:

// 1
func load() async {
    // 2
    var drinks = await store.load()
    
    // Drop old drinks
    drinks.removeOutdatedDrinks()
            
    // Assign loaded drinks to model
    currentDrinks = drinks
    await drinksUpdated()
            
    // Load new data from HealthKit.
    guard await healthKitController.requestAuthorization() else {
        logger.debug("Unable to authorize HealthKit.")
        return
    }
        
    await self.healthKitController.loadNewDataFromHealthKit()
}
  1. 通过在函数后使用
    async 关键字,标记这是一个异步函数。
  2. 在异步函数内执行方法,使用
    await 关键字可以像同步代码一样调用其他异步函数

错误处理

Async/await 中建议使用 try/catch 捕获并抛出异常:

extension HKHealthStore {

    @available(iOS 15.0, *)
    public func requestAuthorization(toShare typesToShare: Set<HKSampleType>, read typesToRead: Set<HKObjectType>) async throws


public func requestAuthorization() async -> Bool {
        guard isAvailable else { return false }
        
        do {
            try await store.requestAuthorization(toShare: types, read: types)
            self.isAuthorized = true
            return true
        } catch let error {
            self.logger.error("An error occurred while requesting HealthKit Authorization: /(error.localizedDescription)")
            return false
        }
    }

注意事项

同步函数不直接调用 async 函数,需要使用 async创建一个异步任务。

public func addDrink(mgCaffeine: Double, onDate date: Date) {
    logger.debug("Adding a drink.")
    
    // Create a local array to hold the changes.
    var drinks = currentDrinks
    
    // Create a new drink and add it to the array.
    let drink = Drink(mgCaffeine: mgCaffeine, onDate: date)
    drinks.append(drink)
    
    // Get rid of any drinks that are 24 hours old.
    drinks.removeOutdatedDrinks()
    
    currentDrinks = drinks
    
    // Save drink information to HealthKit.
    async {
        await self.healthKitController.save(drink: drink)
        await self.drinksUpdated()
    }
}

async 函数可以调用其他 async 函数,也可以直接调用普通的同步函数:

 async {
    // Check for updates from HealthKit.
    let model = CoffeeData.shared
    
    let success = await model.healthKitController.loadNewDataFromHealthKit()
        
    if success {
        // Schedule the next background update.
        scheduleBackgroundRefreshTasks()
        self.logger.debug("Background Task Completed Successfully!")
    }
    
    // Mark the task as ended, and request an updated snapshot, if necessary.
    backgroundTask.setTaskCompletedWithSnapshot(success)
}

如果 sync  函数和 async 函数可以用同样的方式调用,编译器会根据当前的上下文决定调用哪个函数。这在从同步函数迁移到异步函数时将会非常有用,我们可以写出同样命名的异步函数后一步一步迁移到异步环境中。

func doSomething(completionHandler: ((String) -> Void)? = nil) {
    print("test1")
}

func doSomething() async -> String {
    print("test2")
    return ""
}

override func viewDidLoad() {
    super.viewDidLoad()
    
    doSomething()      // print test1
        
    async {
        await doSomething()  // print test2
    }
}

一个 async 的 protocol 可以由 sync 或者 async 的函数满足,但是 syncprotocol 无法被 async 函数满足:

protocol Asynchronous {
  func f() async
}

protocol Synchronous {
  func g()
}

struct S1Asynchronous {
  func f() async { } // okay, exactly matches
}

struct S2Asynchronous {
  func f() { } // okay, synchronous function satisfying async requirement
}

struct S3Synchronous {
  func g() { } // okay, exactly matches
}

struct S4Synchronous {
  func g() async { } // error: cannot satisfy synchronous requirement with an async function
}

一个做晚餐的案例

举个例子,假设我们需要做晚餐,那么要处理蔬菜,腌制肉,预热烤箱,编写为异步代码如下:

func chopVegetables() async throws -> [Vegetable] { ... }
func marinateMeat() async -> Meat { ... }
func preheatOven(temperature: Double) async throws -> Oven { ... }

// ...

func makeDinner() async throws -> Meal {
  let veggies = try await chopVegetables()  // 处理蔬菜
  let meat = await marinateMeat()           // 腌制肉
  let oven = try await preheatOven(temperature: 350//预热烤箱

  let dish = Dish(ingredients: [veggies, meat])   // 把蔬菜和肉装盘
  return try await oven.cook(dish, duration: .hours(3))  // 用烤箱做出晚餐
}

可以看到,代码中三个步骤都是异步执行的,但仍然是顺序执行的,这使得做晚餐的时间变长了。为了让晚餐的准备时间变短,我们需要让处理蔬菜、腌制肉、预热烤箱几个步骤并发执行。为了解决这个问题,Swift 5.5 引入了一个新的概念:结构化并发

结构化并发

Async and Await in Swift 5.5

结构化并发是一种编程范式,旨在通过使用结构化的并发编程方法来提高计算机程序的清晰度、质量和研发效能。核心理念是通过具有明确入口和出口点并确保所有生成的子任务在退出前完成的控制流构造来封装并发执行任务(这里包括内核和用户线程和进程)。这种封装允许并发任务中的错误传播到控制结构的父作用域,并由每种特定计算机语言的本机错误处理机制进行管理。尽管存在并发性,但它允许控制流通过源代码的结构保持显而易见。为了有效,这个模型必须在程序的所有级别一致地应用——否则并发任务可能会泄漏、成为孤立的或无法正确传播运行时错误。(来自维基百科)

概念的定义比较复杂。在我们的日常业务开发中,有些场景是希望多个任务并发执行,在所有任务都完成后,再进行一些操作。其实这就是所谓的结构化并发,只不过在过去,并没有一个编程模型的支持,大部分情况下我们需要靠一些同步机制去实现这个效果。

任务集合和子任务

Swift 引入了任务集合和子任务概念实现了结构化并发:

func makeDinner() async throws -> Meal {
 var veggies: [Vegetable]?
  var meat: Meat?
  var oven: Oven?
  enum CookingStep {
    case veggies([Vegetable])
    case meat(Meat)
    case oven(Oven)
  }
  
 // 1
 try await withThrowingTaskGroup(of: CookingStep.self) { group in
    // 2
    group.async {
      try await .veggies(chopVegetables())
    }
    group.async {
      await .meat(marinateMeat())
    }
    group.async {
      try await .oven(preheatOven(temperature: 350))
    }
                                             
    for try await finishedStep in group {
      switch finishedStep {
        case .veggies(let v): veggies = v
        case .meat(let m): meat = m
        case .oven(let o): oven = o
      }
    }
  }
  let dish = Dish(ingredients: [veggies!, meat!])
  return try await oven!.cook(dish, duration: .hours(3))
}
  1. withThrowingTaskGroup 创建了一个任务组 Task Group。
  2. group.async 向任务组中添加任务 在这个异步的上下文中,我们创建了一个任务组,任务组里搭载了三个异步任务,即它的子任务。任务组必须等她所有的子任务都结束才会结束。这样的依赖关系最终会形成一个树形的机构。

Task group 定义了一个生命周期,可以在其中以异步调用的方式创建新的子任务。与所有子任务一样,group 生命周期内的子任务必须在生命周期结束时完成,如果父任务退出并抛出错误,则将首先隐式取消子任务。另外, group.async  中不能直接修改捕获的父生命周期中的变量。

let bindings

虽然 Task Group 非常强大,但它们在使用上还是比较麻烦的。Swift 也提供了 async let , 可以用一种更加简单的方式来创建子任务并等待子任务的执行结果:

func makeDinner() async throws -> Meal {
  async let veggies = chopVegetables()
  async let meat = marinateMeat()
  async let oven = preheatOven(temperature: 350)

  let dish = Dish(ingredients: await [try veggies, meat]) // 这里的数组默认创建了一个 Task Group
  return try await oven.cook(dish, duration: .hours(3))
}

let bingds 的限制

async let 简单强大,但也有很多限制。async let 只能在 async function 或者 async closure 中使用:

func greet() async -> String { "hi" }

func asynchronous() async {
  async let hello = greet()
  // ...
  await hello
}

async let 不允许在顶层代码、同步函数中使用:

async let top = ... // error: 'async let' in a function that does not support concurrency

func sync() { // note: add 'async' to function 'sync()' to make it asynchronous
  async let x = ... // error: 'async let' in a function that does not support concurrency
}

func syncMe(later: () -> String) { ... }
syncMe {
  async let x = ... // error: invalid conversion from 'async' function of type '() async -> String' to synchronous function type '() -> String'
}

async let 可以和元组等结合使用:

func left() async -> String { "l" }
func right() async -> String { "r" }

async let (l, r) = (left(), right())

await l // at this point `r` is also assumed awaited-on

async 函数中定义了 async let,但是并未 await,那会出现隐式的 await 调用:

func go() async {
  async let f = fast() // 300ms
  async let s = slow() // 3seconds
  return "nevermind..."
  // implicitly: cancels f
  // implicitly: cancels s
  // implicitly: await f
  // implicitly: await s
}

当我们使用 async let 创建了一个 async 对象后,它会作为一个子任务,加入到 go 的子任务中,形成了了一个树形结构。根据结构化并发,go  函数想要结束,就必须等待所有子任务都结束。因此这个时候就相当于编译器会默认的帮我们调用对于 fastslowawait。但是一般不但不建议大家用这种隐式 awiat,隐式 awiat 可能会导致对于生命周期的理解出问题。

Actors

Swift 5.5 中的并发模型旨在提供一个安全的编程模型,可以静态检测数据竞争和其他常见的并发错误。结构化并发为函数和闭包提供多线程竞争安全性保障。该模型适用于许多常见的设计模式,包括并行映射和并发回调模式等,但仅限于处理由闭包捕获的状态。

Swift 中的类提供了一种机制来声明多线程共享的可变状态。然而,众所周知,类很难在并发程序中正确使用,需要通过一些类似锁、信号量等同步机制以避免数据竞争。最理想的情况是既提供使用共享可变状态的能力,同时仍然能够对数据竞争和其他常见并发错误的静态检测。

为此 Swift 5.5 引入了新的并发模型 Actor , 它通过数据隔离保护内部的数据,确保在给定时间只有一个线程可以访问该数据。

Actor 模型最早是出现在分布式系统中,用于解决系统中多种的并发冲突。

Actor 类似于一个信箱:

  • 状态私有:外部无法直接去访问 Actor 的内部数据
  • 只能接收和处理消息:外部想要与 Actor 通信只能发送消息
  • 每个 Actor一次只执行一个消息:Actor 内部在一个消息队列中处理消息,保证了线程安全
Async and Await in Swift 5.5
  • mailbox: 存储 message 的队列
  • Isolated State: Actor 的状态,内部变量等
  • message: 类似于 OOP 的方法调用的参数

Actor 与传统 OOP 区别

Async and Await in Swift 5.5
  • Actor 状态私有,不可共享
  • Actor 通过消息进行通信,不需要传递执行线程
  • Actor 内部串行处理消息,可以保障内部状态和变量的正确性
Async and Await in Swift 5.5
  • OOP 状态可共享,外部可以访问和改变
  • OOP 方法调用会传递执行线程
  • OOP 方法调用不保证执行线程,因此需要使用锁来确保内部状态和变量的正确性

Actor 使用示例

在下面的代码中,多线程环境下 RiskyCollector 类的访问并不安全,可能会出现访问冲突:

class RiskyCollector {
   var deck: Set<String>
   
   init(deck: Set<String>) {
       self.deck = deck
   }
   
   func send(card selected: String, to person: RiskyCollector) -> Bool {
       guard deck.contains(selected) else { return false }
   
       deck.remove(selected)
       person.transfer(card: selected)
       return true
   }
   
   func transfer(card: String) {
       deck.insert(card)
   }
}

在 Swift 5.5 中引入了 actor 关键字,可以创建 Actor 对象。Actor 对象有两个限制:

  1. 所有的对外暴露的方法必须是
    async
  2. Actor 的所有的属性对于外部是无法直接访问的,必须通过他的方法去进行访问。
actor SafeCollector {
    var deck: Set<String>
    
    init(deck: Set<String>) {
        self.deck = deck
    }
    
          // 1
    func send(card selected: String, to person: SafeCollector) async -> Bool {
        guard deck.contains(selected) else { return false }
    
        deck.remove(selected)
              // 2
        await person.transfer(card: selected)
        return true
    }
    
    func transfer(card: String) {
        deck.insert(card)
    }
}
  1. send() 方法标示为
    async,该方法需要等待
    person
    transfer 完成
  2. 尽管
    transfer 并不是
    async 方法, 但是在调用其他对象的方法的时候仍然需要通过
    await 进行调用通过使用
    actor
    SafeCollector 实现了线程安全。

Actor isolation

再来看另外一个示例代码:

actor BankAccount {
  let accountNumber: Int
  var balance: Double

  init(accountNumber: Int, initialDeposit: Double) {
    self.accountNumber = accountNumber
    self.balance = initialDeposit
  }
    
  enum BankErrorError {
    case insufficientFunds
  }
  
  func transfer(amount: Double, to other: BankAccount) throws {
    if amount > balance {
      throw BankError.insufficientFunds
    }

    print("Transferring /(amount) from /(accountNumber) to /(other.accountNumber)")

    balance = balance - amount
    other.balance = other.balance + amount  // error: actor-isolated property 'balance' can only be referenced on 'self'
  }
}

如果 BankAccount 是一个 class ,那上述代码是没有任何编译问题的,但是会出现多线程问题。在 actor 中尝试访问 other.balance 会出发编译错误,因为 balance 只能通过 self 访问。可以看到,Actor 将错误从运行时提前到了编译期间,避免的潜在的多线程问题。解决上面的问题,需要将对 balance 的处理封装成一个 async 方法:

extension BankAccount {
  func deposit(amount: Double) async {
    assert(amount >= 0)
    balance = balance + amount
  }
  
  func transfer(amount: Double, to other: BankAccount) async throws {
    assert(amount > 0)

    if amount > balance {
      throw BankError.insufficientFunds
    }

    print("Transferring /(amount) from /(accountNumber) to /(other.accountNumber)")

    // Safe: this operation is the only one that has access to the actor's isolated
    // state right now, and there have not been any suspension points between
    // the place where we checked for sufficient funds and here.
    balance = balance - amount
    
    // Safe: the deposit operation is placed in the `other` actor's mailbox; when
    // that actor retrieves the operation from its mailbox to execute it, the
    // other account's balance will get updated.
    await other.deposit(amount: amount)
  }
}

可重入的 Actor

Actor 的函数是可重入的,当一个 actor 的函数挂起, 重入机制可以允许挂起恢复前其他工作进行执行,这个就是交替执行,可以解决死锁的问题。

actor Person {
 let friend: Friend
 
 // actor-isolated opinion
 var opinion: Judgment = .noIdea

 func thinkOfGoodIdea() async -> Decision {
   opinion = .goodIdea                       // <1>
   await friend.tell(opinion, heldBy: self)  // <2>
   return opinion // ?                     // <3>
 }

 func thinkOfBadIdea() async -> Decision {
   opinion = .badIdea                       // <4>
   await friend.tell(opinion, heldBy: self// <5>
   return opinion // ?                     // <6>
 }
}

let goodThink = detach { await person.thinkOfGoodIdea() }  // runs async
let badThink = detach { await person.thinkOfBadIdea() } // runs async

let shouldBeGood = await goodThink.get()
let shouldBeBad = await badThink.get()

await shouldBeGood // could be .goodIdea or .badIdea ☠️
await shouldBeBad

Actor with Objc

对于 ObjC 的 actor 对象,目前只支持异步方法和标记了 nonisolated 的同步方法。这个是编译器现有的设计,不过未来可能会改变。

@objc actor MyActor {
   @objc func synchronous() { } // error: part of actor's isolation domain
   @objc func asynchronous() async { } // okay: asynchronous, exposed to Objective-C as a method that accepts a completion handler
   @objc nonisolated func notIsolated() { } // okay: non-isolated
}

Global Actor

在应用程序中,主线程通常负责执行主要事件处理循环,该循环处理来自各种来源的事件并将它们传递给应用程序代码。图形应用程序通常在主线程上传递用户交互事件(键盘按下、触摸交互),并且要求用户界面的任何状态更新也发生在主线程。

class ViewController {
   func refreshUI() -> Bool {
       guard Thread.isMainThread else {
           return false
       }
   
       print("updating ui…")
       return true
   }
}

Global Actor 将 actor 隔离的概念扩展到了全局状态,即使状态和函数分散在许多不同的模块中,Global Actor 可以在并发程序中安全地使用全局变量,例如 Swift 提供的 @MainActor 限制属性和方法只能在主线程访问:

class ViewController {
    @MainActor func refreshUI() {
        print("updating ui…")
    }
}

@MainActor var globalTextSize: Int

@MainActor func increaseTextSize() {
  globalTextSize += 2   // okay:
}

func notOnTheMainActor() async {
  globalTextSize = 12  // error: globalTextSize is isolated to MainActor
  increaseTextSize()   // error: increaseTextSize is isolated to MainActor, cannot call synchronously
  await increaseTextSize() // okay: asynchronous call hops over to the main thread and executes there
}

Global 也支持自定义,有兴趣可以查看文末的参考链接。Actor 和 Class 对比

  • Actor 和 Class 共同点有:
  • 两者都是引用类型,因此它们可用于共享状态。
    • 它们可以有方法、属性、初始值设定项和下标。
    • 它们可以实现协议。
    • 任何静态属性和方法在这两种类型中的行为都相同。
  • 除了 Actor isolation之外,Actor 和 Class 之间还有另外两个重要的区别:
  • Actor 目前不支持继承,这使得它们的初始化器更加简单——不需要方便的初始化器、覆盖、
    final 关键字等等。这在未来可能会改变。
    • 所有
      actor  都隐含地遵守一个新的
      actor 协议;没有其他具体类型可以使用它。

其他异步改动

Swift 5.5 除了引入上述三个重要的异步改动外,还带来了一些便利的功能。

Async sequence

顾名思义,Async sequence 就是在异步语境下的序列:

struct Counter : AsyncSequence {
  let howHigh: Int

  struct AsyncIterator : AsyncIteratorProtocol {
    let howHigh: Int
    var current = 1
    mutating func next() async -> Int? {
      // We could use the `Task` API to check for cancellation here and return early.
      guard current <= howHigh else {
        return nil
      }

      let result = current
      current += 1
      return result
    }
  }

  func makeAsyncIterator() -> AsyncIterator {
    return AsyncIterator(howHigh: howHigh)
  }
}

for await i in Counter(howHigh: 3) {
  print(i)
}

/*
Prints the following, and finishes the loop:
1
2
3
*/

编译器为 Async Sequence 提供了特殊的语法支持,让我们可以像访问数组一样去访问一个异步序列。

Readonly properties

Swift 5.5 中只读计算属性也可以支持 async 了,使用方法和异步函数类似,可以按照下图所示代码使用:

enum FileErrorError {
    case missing, unreadable
}

struct BundleFile {
    let filename: String
    
    var contents: String {
        get async throws {
            guard let url = Bundle.main.url(forResource: filename, withExtension: nilelse {
                throw FileError.missing
            }
    
            do {
                return try String(contentsOf: url)
            } catch {
                throw FileError.unreadable
            }
        }
    }
}

func printHighScores() async throws {
    let file = BundleFile(filename: "highscores")
    try await print(file.contents)
}

Continuations

Swift 5.5 提供了 Continuations 让我们可以把已有的基于闭包的方法转换成 async 方法。

func fetchLatestNews(completion: @escaping ([String]) -> Void) {
    DispatchQueue.main.async {
        completion(["Swift 5.5 release""Apple acquires Apollo"])
    }
}

Continuations 可以让开发者在旧的回调函数和 async 函数之间创建一个桥接,以便开发者将旧代码包装在更现代的 API 中。例如,withCheckedContinuation()`` 函数创建一个新的continuation,它可以运行你想要的任何代码,然后在你准备好时调用resume(returning:)“ 来返回一个值,这就是完成处理程序闭包的一部分。基于此,我们可以改写出 async 版的 fetchLatestNews:

func fetchLatestNews() async -> [String] {
    await withCheckedContinuation { continuation in // 相当于挂起点
        fetchLatestNews { items in
            continuation.resume(returning: items)   // 恢复
        }
    }
}

func printNews() async {
    let items = await fetchLatestNews()
    
    for item in items {
        print(item)
    }
}

注意这里的同步函数和异步函数是同名的。在 printNews 中,编译器会帮我们判断,执行异步的 fetchLatestNews。Continuations 不仅可以直接用于桥接 callback 相关的旧代码,也可以作为变量保存下来,进行更加复杂的桥接操作,比如像 delegate 异步的桥接等:

class MyOperationOperation {
 let continuation: UnsafeContinuation<OperationResultNever>
 var result: OperationResult?

 init(continuation: UnsafeContinuation<OperationResultNever>) {
   self.continuation = continuation
 }

 /* rest of operation populates `result`... */

 override func finish() {
   continuation.resume(returning: result!)
 }
}

func doOperation() async -> OperationResult {
 return await withUnsafeContinuation { continuation in
   MyOperation(continuation: continuation).start()
 }
}

我们也可以根据父任务的取消来取消桥接的任务,像上面我们使用 Continuation 来桥接了 URLSession 的请求,当 task 取消的时候,取消下载任务:

func download(url: URL) async throws -> Data? {
 var urlSessionTask: URLSessionTask?

 return try Task.withCancellationHandler {
   urlSessionTask?.cancel()
 } operation: {
   let result: Data? = try await withUnsafeThrowingContinuation { continuation in
     urlSessionTask = URLSession.shared.dataTask(with: url) { data, _, error in
       if case (let cancelled as NSURLErrorCancelled)? = error {
         continuation.resume(returning: nil)
       } else if let error = error {
         continuation.resume(throwing: error)
       } else {
         continuation.resume(returning: data)
       }
     }
     urlSessionTask?.resume()
   }
   if let result = result {
     return result
   } else {
     Task.cancel()
     return nil
   }
 }
}

总结

更新项 描述链接 适用系统
async/await https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md iOS15及以上
async sequences https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md iOS15及以上
Effectful read-only properties https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md iOS15及以上
Structured Concurrency https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md iOS15及以上
async let https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md iOS15及以上
Continuations for interfacing async tasks with synchronous code https://github.com/apple/swift-evolution/blob/main/proposals/0300-continuation.md iOS15及以上
Actors https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md iOS15及以上
Global Actors https://github.com/apple/swift-evolution/blob/main/proposals/0316-global-actors.md iOS15及以上
Sendable and @Sendable closures https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md 不限制
#if for postfix member expressions https://github.com/apple/swift-evolution/blob/main/proposals/0308-postfix-if-config-expressions.md 不限制
Codable synthesis for enums with associated values https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md 不限制
lazy now works in local contexts 不限制
Extend Property Wrappers to Function and Closure Parameters https://github.com/apple/swift-evolution/blob/main/proposals/0293-extend-property-wrappers-to-function-and-closure-parameters.md 不限制
Extending Static Member Lookup in Generic Contexts https://github.com/apple/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md 不限制

Swift 5.5 带来了很多振奋人心的更新:Async/Await 可以降低异步编程的复杂度,书写出更加简洁高效可读性强的异步代码逻辑,结构化并发和 Actor 编程模型的引入可以让开发者写出更加安全的并发代码,Continuation 可以让开发者快速地将已有的代码迁移到新的异步特性中。让人失望的是 Concurrency 相关的特性由于线程模型的支持,只能在 iOS15 及以上的设备中运行, 尽管此次更新的限制诸多,但是我仍然很看好 Swift,相信未来 Swift 这门语言可以给我们带来更多的惊喜。

参考

  • 解决并发编程之痛的良药–结构化并发编程:https://zhuanlan.zhihu.com/p/108759542
  • https://github.com/twostraws/whats-new-in-swift-5-5
  • Continuations for interfacing async tasks with synchronous code
  • https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md
  • https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md
  • https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md#cancellation
  • https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md#protocol-conformance
  • https://github.com/apple/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md
  • https://github.com/apple/swift-evolution/blob/main/proposals/0317-async-let.md
  • https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
  • How the Actor Model Meets the Needs of Modern, Distributed Systems:https://doc.akka.io/docs/akka/current/typed/guide/actors-intro.html
  • Why modern systems need a new programming model:https://doc.akka.io/docs/akka/current/typed/guide/actors-motivation.html

内推

对这次分享感兴趣的朋友可以加入讲师所在的开发团队,一起玩转 Swift~

Async and Await in Swift 5.5

本文分享自微信公众号 – 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

{{o.name}}


{{m.name}}

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/80414.html

(0)
上一篇 2021年8月12日 09:08
下一篇 2021年8月12日 09:08

相关推荐

发表回复

登录后才能评论