Swift 中异步 即将支持的Async/Await

原文:Async/Await for Swift

介绍 :现代Cocoa开发涉及到很多使用闭包和completion handlers的异步编程,但这些API很难用。当使用许多异步操作,中间有错误处理或异步调用的控制流的时候,问题会变得特别复杂。针对上面的问题,提出了一个对swift的扩展,让swift处理异步更加自然,不易出错。 本文介绍了一个Swift一流的协程(维基百科)模型。函数可以使用async关键词,让编写涉及异步操作的复杂逻辑通过编译器自动生成必要的闭包和状态机来实现该逻辑。 这一点非常重要,首先它提出了runtime-agnostic(运行时无感知的)完全并发的编译器支持。并不是要建立一个新的运行时模型(比如“actors”的参与者的模型,另外提一句,之前喵神Swift 并行编程现状和展望 - async/await 和参与者模式有提到的swift异步编程可能会使用actors模型看来并不会,而是使用async关键词)。并且它和GCD一样能很好的使用pthread或者其他的API。此外,与其他语言的设计不同,它独立于特定的协调机制(例如futures或channels),将这些机制作建立为语言库的一个特征。唯一需要做的就是利用运行时来支持转换和操作隐式生成的闭包,而这些工作将会通过编译器特性来支持实现对应的逻辑。这些内容是Chris LattnerJoe GroffOleg Andreev撰写的早期提案中得到了一些启示,经过大改重写的提议。

动机: Completion handlers并不理想
为了展示Completion遭遇的事情,并且这些问题还是很重要的.我们来看看在Cocoa(包括服务端/云)编程过程中经常碰的问题。
问题1: 厄运金字塔(回调地狱 callback hell) 在嵌套块中执行一些简单的操作显得特别不自然。 下面是例子:


func processImageData1(completionBlock: (result: Image) -> Void) { 

    loadWebResource("dataprofile.txt") { 

        dataResource in loadWebResource("imagedata.dat") {

             imageResource in decodeImage(dataResource, imageResource) { 

                imageTmp in dewarpAndCleanupImage(imageTmp) {

                     imageResult in completionBlock(imageResult)

                 }

             }

         }

     }

}

processImageData1 { image in display(image)} 

这个嵌套的回调使得问题追踪变得很困难,并且闭包堆栈还引带来一步的影响。

问题2:错误处理错误处理也变得困难,并且很麻烦。Swift 2为同步代码引入了一个错误处理的模型,但是基于回调的接口这么处理并不好.


func processImageData2(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 in guard let imageResult = imageResult else {

                         completionBlock(nil, error) return } completionBlock(imageResult)

                     }

                 }

             }

         }

    }

processImageData2 { 

    image, error in guard let image = image else {

         error("No image today")

          return

     }

 display(image)

}

问题3:条件执行很困难并且容易出错条件地执行一个异步函数是非常的痛苦。或许最好的办法是在条件执行的闭包(协助的闭包)中写入一半的代码,如下所示:


func processImageData3(recipient: Person, completionBlock: (result: Image) -> Void) {

     let continuation: (contents: image) -> Void = {

     // ... continue and call completionBlock eventually

     }

 if recipient.hasProfilePicture { 

     continuation(recipient.profilePicture)

 }

 else {

     decodeImage { 

        image in continuation(image) 

         }

     }

}

问题4:很容易出错很容易就return掉了,忘记调用那个协助的block,并且这个问题很难调试


func processImageData4(completionBlock: (result: Image?, error: Error?) -> Void) {     loadWebResource("dataprofile.txt") { 

        dataResource, error in guard let dataResource = dataResource else { 

             return // <- 忘记调用block 

         }

        loadWebResource("imagedata.dat") { 

            imageResource, error in guard let imageResource = imageResource else {

                 return // <- 忘记调用block 

             } 

         ...

         }

    }

}

当你不忘记调用的时候,你还有可以忘记会要return。庆幸的是,在某种程度上guard语法可以帮助你,但并不是每一次都奏效。


func processImageData5(recipient:Person, completionBlock: (result: Image?, error: Error?) -> Void) {

     if recipient.hasProfilePicture { 

         if let image = recipient.profilePicture { 

             completionBlock(image) // <- 调用block之后忘了 return

             }

         }

         ...

}

问题5:由于completion handlers很笨拙而定义了太多的API(抱歉这里没有看太明白直接贴原文)This is hard to quantify, but the authors believe that the awkwardness of defining and using asynchronous APIs (using completion handlers) has led to many APIs being defined with apparently synchronous behavior, even when they can block. This can lead to problematic performance and responsiveness problems in UI applications - e.g. spinning cursor. It can also lead to the definition of APIs that cannot be used when asynchrony is critical to achieve scale, e.g. on the server.

问题6:其他“可恢复”的行为很难定义这里“可恢复”行为比如下面:要编写生成一组数的平方的列表的代码,可以这样写:for i in 1...10 { print(i*i)}、但是,如果你想把它写成Swift sequence,你必须将其定义成可迭代的方式去生成值。 有多种方法可以做到这一点(例如,使用AnyIterator或序列(state:next :)函数),但是没有一种方法能够实现命令式的清晰和明显。相反,具有generators的语言允许你写更接近这个的东西:


func getSequence() -> AnySequence{

     let seq = sequence {

         for i in 1...10 {

             yield(i*i)

         }

     } 

 return AnySequence(seq)

}

编译器有责任通过生成状态机将函数转换为迭代式生成值的形式。

建议的方案:协程这些问题在许多系统和许多语言中都已经面临,而协程的抽象是解决它们的标准方法。在不深入理论的情况下,协程是允许函数返回值或被暂停的基本函数的扩展。它们可以用来实现生成器,异步模型和其他功能 - 理论,实现和优化都有大量的工作。这个提议增加了对Swift的一般协程支持,使用最常见的方式:定义和使用异步API,消除了许多使用完成处理程序的问题。关键词的选择(async与yields)是一个需要解决的话题,但与模型的核心语义无关。最后,请参阅“Alternate Syntax Options”了解此其中的语法操作。很重要的一点是你需要提前知道这个提议的协程模型并不与特定系统上的任何特定的并发接口相关:你可以把它看作完成completion handlers的语法糖。像其他系统一样,这意味着引入协程并不会改变completion handlers执行的的队列。异步语法现在,函数的类型可以是正常的或者是可以throw的,这个提议指出函数类型同样可以使用async。下面的函数都是正确的


 (Int) -> Int // #1: Normal function

 (Int) throws -> Int // #2: Throwing function

 (Int) async -> Int // #3: Asynchronous function

 (Int) async throws -> Int // #4: Asynchronous function, can also throw

就像普通函数(#1)隐式转换为抛出函数(#2),异步函数(#3)隐式转换为抛出异步函数(#4)。在函数的声明方面,你可以声明一个函数是异步的,就像你声明它是抛出一样,但是使用async关键字:


func processImageData() async -> Image { ... }

//语法类似于这个:

func processImageData(completionHandler: (result: Image) -> Void) { ... }

调用异步函数可以隐式挂起当前的协程。 为了让代码维护者明白这一点,你需要用新的await关键字“标记”调用异步函数的表达式(类似于用try来标记包含抛出调用的子表达式)。 把这两部分放在一起,第一个例子(上面回调嵌套的版本)可以用更自然的方式重写:


func loadWebResource(_ path: String) async -> Resource

func decodeImage(_ r1: Resource, _ r2: Resource) async -> Image

func dewarpAndCleanupImage(_ i : Image) async -> Image

func processImageData1() async -> Image { 

     let dataResource = await loadWebResource("dataprofile.txt") 

     let imageResource = await loadWebResource("imagedata.dat")

     let imageTmp = await decodeImage(dataResource, imageResource)

     let imageResult = await dewarpAndCleanupImage(imageTmp)

     return imageResult

}

在内部,编译器使用上面的例子processImageData1中的嵌套闭包来重写这段代码。注意了,每个操作只有在前一个操作完成后才会启动,但是每个调用异步函数的站点都可以暂停当前函数的执行。最后,只允许从另一个异步函数或闭包中调用异步函数。这里使用Swift 2错误处理的模型你不能调用抛出函数,除非你在抛出函数中,或者在do / catch块中。进入和退出异步代码在通常情况下,异步代码应该调用由的其他异步代码完成,但是在某个时刻,异步过程需要自己控制同步上下文的异步过程,需要能够暂停自己,并允许其控制上下文的继续。 我们需要一些函数来启用和暂停异步上下文:


// NB: Names subject to bikeshedding. These are low-level primitives that most

// users should not need to interact with directly, so namespacing them

// and/or giving them verbose names unlikely to collide or pollute code

// completion (and possibly not even exposing them outside the stdlib to begin

// with) would be a good idea.

/// Begins an asynchronous coroutine, transferring control to `body` until it

/// either suspends itself for the first time with `suspendAsync` or completes,

/// at which point `beginAsync` returns. If the async process completes by

/// throwing an error before suspending itself, `beginAsync` rethrows the error.func beginAsync(_ body: () async throws -> Void) rethrows -> Void

/// Suspends the current asynchronous task and invokes `body` with the task's

/// continuation closure. Invoking `continuation` will resume the coroutine

/// by having `suspendAsync` return the value passed into the continuation.

/// It is a fatal error for `continuation` to be invoked more than once.func suspendAsync( _ body: (_ continuation: @escaping (T) -> ()) -> ()) async -> T

/// Suspends the current asynchronous task and invokes `body` with the task's

/// continuation and failure closures. Invoking `continuation` will resume the

/// coroutine by having `suspendAsync` return the value passed into the

/// continuation. Invoking `error` will resume the coroutine by having

/// `suspendAsync` throw the error passed into it. Only one of

/// `continuation` and `error` may be called; it is a fatal error if both are

/// called, or if either is called more than once.

func suspendAsync(

     _ body: (_ continuation: @escaping (T) -> (), _ 

    error: @escaping (Error) -> ()

) -> ()) async throws -> T

这些与"delimited continuations"的“shift”和“reset”类似。 这些允许非异步函数来调用异步函数。 例如,用completion handlers编写的@IBAction:


@IBAction func buttonDidClick(sender:AnyObject) {

 // 1 processImage(completionHandler: {(image) in 

 // 2 imageView.image = image }) 

 // 3}

This is an essential pattern, but is itself sort of odd: an async operation is being fired off immediately (#1), then runs the subsequent code (#3), and the completion handler (#2) runs at some time later -- on some queue (often the main one). This pattern frequently leads to mutation of global state (as in this example) or to making assumptions about which queue the completion handler is run on. Despite these problems, it is essential that the model encompasses this pattern, because it is a practical necessity in Cocoa development. With this proposal, it would look like this:


@IBAction func buttonDidClick(sender:AnyObject) { 

 // 1 beginAsync { 

 // 2 let image = await processImage() imageView.image = image

 }

 // 3

}

这些函数能通过基于回到的api封装成异步的协程api


// Legacy callback-based API

func getStuff(completion: (Stuff) -> Void) { ... }

// Swift wrapper

func getStuff() async -> Stuff {

     return await suspendAsync {

         continuation in getStuff(completion: continuation) 

    }

}

比如 libdispatch 和 pthread 这种函数式的并发的库 同样可以通过协程的形式很友好的提供接口


extension DispatchQueue {

 /// Move execution of the current coroutine synchronously onto this queue.

     func syncCoroutine() async -> Void { 

         await suspendAsync {

             continuation in sync { continuation

         }

     }

 }

 /// Enqueue execution of the remainder of the current coroutine 

 /// asynchronously onto this queue.

 func asyncCoroutine() async -> Void { 

     await suspendAsync {

         continuation in async { 

            continuation

         }

     }

 }

}

func queueHopping() async -> Void { 

     doSomeStuff()

     await DispatchQueue.main.syncCoroutine() 

     doSomeStuffOnMainThread() 

    await backgroundQueue.asyncCoroutine()  

    doSomeStuffInBackground()

}

也可以建立协调协程的通用抽象形式。 其中最简单的就是future(类似promise),future中的值可能还没有没有resolved(不明白的同学可以搜索一下promise 或者future的原理,有兴趣的同学可以去看一下promiseKit)。 Future的确切设计超出了这个提议的范围(应该是它自己的后续提议),但是概念证明的例子可能是这样的:


class Future{

  private enum Result { case error(Error), value(T) }

  private var result: Result? = nil

  private var awaiters: [(Result) -> Void] = []

// Fulfill the future, and resume any coroutines waiting for the value.

  func fulfill(_ value: T) {

    precondition(self.result == nil, "can only be fulfilled once")

    let result = .value(value)

    self.result = result

    for awaiter in awaiters {

      // A robust future implementation should probably resume awaiters

      // concurrently into user-controllable contexts. For simplicity this

      // proof-of-concept simply resumes them all serially in the current

      // context.

      awaiter(result)
    
    }

    awaiters = []

  }

  // Mark the future as having failed to produce a result.

  func fail(_ error: Error) {

    precondition(self.result == nil, "can only be fulfilled once")

    let result = .error(error)

    self.result = result

    for awaiter in awaiters {

      awaiter(result)

    }

    awaiters = []

  }

  func get() async throws -> T {

    switch result {

      // Throw/return the result immediately if available.

      case .error(let e)?:

        throw e

      case .value(let v)?:

        return v

      // Wait for the future if no result has been fulfilled.

      case nil:

        return await suspendAsync {
           continuation, error in

          awaiters.append({

          switch $0 {

            case .error(let e): error(e)

            case .value(let v): continuation(v)

          }

        })

    }

  }

  }

  // Create an unfulfilled future.

  init() {}

  // Begin a coroutine by invoking `body`, and create a future representing

  // the eventual result of `body`'s completion.

  convenience init(_ body: () async -> T) {

    self.init()

    beginAsync {

      do {

        self.fulfill(await body())

      } catch {

        self.fail(error)

      }

    }

  }

}

重申一下,众所周知,这个具体的实现有性能和API的缺点,只是简单地描述一下像这样的抽象可以建立在async/await之上。

Futrues允许并行执行,在需要时将等待调用的结果移动到结果中,并将并行调用包装在各个Future对象中:(这里提一下,及时从nodejs的实践中我的体会是通过单纯的promise或者future链式调用的时候回出现过个连续的参数无法再同一个作用域中使用,所以封装出async和await是很必要的一个方式,有兴趣的同学可以看es7中大家关于async和await的讨论)


func processImageData1a() async -> Image {

let dataResource  = Future { await loadWebResource("dataprofile.txt") }

let imageResource = Future { await loadWebResource("imagedata.dat") }

// ... other stuff can go here to cover load latency...

let imageTmp    = await decodeImage(dataResource.get(), imageResource.get())

let imageResult = await dewarpAndCleanupImage(imageTmp)

return imageResult

}

在上面的例子中,前两个操作会一个接一个地开始,而未求值的计算被包装成Future值。 这允许它们全部同时发生(不需要由语言或Future实现定义的方式),并且在解码图像之前,函数将等待它们完成。 请注意,await不会阻止执行流程:如果该值尚未准备就绪,则暂停当前异步函数的执行,并将控制流程传递到堆栈中较高的位置。

其他协调抽象,如 Communicating Sequential Process channels 或者 Concurrent ML events也可以作为协调协程的库来开发; 他们的实现留给读者作为练习。

转换导入的objective-c 的APIs

完整的细节不在本提案的范围之内,但重要的是要增强导入器将基于Objective-C completion-handler的API映射到async中。 相当于NSError **函数作为throws函数导入的转换。 引进这些意味着许多Cocoa API将会更加现代化。

有多种可能的设计与权衡。 完成这项工作通过最根源的方式是以两种形式完成基于completion-handler的API的转换:completion-handler和async之间的转化。 例如:


// Before

- (void) processImageData:(void(^)())completionHandler;

- (void) processImageData:(void(^)(Image* __nonnull image))completionHandler;

- (void) processImageData:(void(^)(Image* __nullable image1, NSError* __nullable error))completionHandler;

- (void) processImageData:(void(^)(Image* __nullable half1, Image* __nullable half2, NSError* __nullable error))completionHandler;

- (void) processImageData:(void(^)(NSError* __nullable error))completionHandler;

上面的声明都是以正常completion-handler的,而且也是以更好的async改写:


func processImageData() async

func processImageData() async -> Image

func processImageData() async throws -> Image

func processImageData() async throws -> (half1: Image, half2: Image)

func processImageData() async throws

应该将一下这些细节定义为引入这一特性过程的一部分,例如:

什么是转化的确切规则?

多个结果函数是否能够自动处理?

在Swift 5模式下completion-handler只能作为async,强制迁移会更好吗?

应该怎样处理non-Void-returning completion handler(例如在URLSession)?

是否应该把用于触发响应事件的异步操作(如IBAction方法)的Void方法应改为async - > Void?

Without substantial ObjC importer work, making a clean break and forcing migration in Swift 5 mode would be the most practical way to preserve overridability, but would create a lot of churn in 4-to-5 migration. Alternatively, it may be acceptable to present the async versions as final wrappers over the underlying callback-based interfaces; this would subclassers to work with the callback-based interface, but there are generally fewer subclassers than callers.

(很难看明白)

如果没有大量的ObjC转换的工作,在Swift 5下进行彻底强制迁移将是保持可覆盖性的最实际的方法,但是会在4到5的迁移中产生大量的改变。或者,可以接受的情况是将async版本作为基于回调的接口上的最终(final)的封装版本;

与现有功能交互

这个提议与Swift中现有的语言特性很契合,下面是几个例子:

错误处理

在Swift 2中引入的错误处理语法自然而然地与这个异步模型相结合。


// Could throw or be interrupted:

func processImageData() async throws -> Image

// Semantically similar to:

func processImageData(completionHandler: (result: Image?, error: Error?) -> Void)