Skip to content

Instantly share code, notes, and snippets.

@lattner
Forked from oleganza/async_swift_proposal.md
Last active October 6, 2024 06:37
Show Gist options
  • Save lattner/429b9070918248274f25b714dcfc7619 to your computer and use it in GitHub Desktop.
Save lattner/429b9070918248274f25b714dcfc7619 to your computer and use it in GitHub Desktop.
Concrete proposal for async semantics in Swift

Async/Await for Swift

Introduction

Modern Cocoa development involves a lot of asynchronous programming using closures and completion handlers, but these APIs are hard to use. This gets particularly problematic when many asynchronous operations are used, error handling is required, or control flow between asynchronous calls gets complicated. This proposal describes a language extension to make this a lot more natural and less error prone.

This paper introduces a first class Coroutine model to Swift. Functions can opt into to being async, allowing the programmer to compose complex logic involving asynchronous operations, leaving the compiler in charge of producing the necessary closures and state machines to implement that logic.

It is important to understand that this is proposing compiler support that is completely concurrency runtime-agnostic. This proposal does not include a new runtime model (like "actors") - it works just as well with GCD as with pthreads or another API. Furthermore, unlike designs in other languages, it is independent of specific coordination mechanisms, such as futures or channels, allowing these to be built as library feature. The only runtime support required is compiler support logic for transforming and manipulating the implicitly generated closures.

This draws some inspiration from an earlier proposal written by Oleg Andreev, available here. It has been significantly rewritten by Chris Lattner and Joe Groff.

Motivation: Completion handlers are suboptimal

To provide motivation for why it is important to do something here, lets look at some of the problems that Cocoa (and server/cloud) programmers frequently face.

Problem 1: Pyramid of doom

Sequence of simple operations is unnaturally composed in the nested blocks. Here is a made up example showing this:

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

This "pyramid of doom" makes it difficult to keep track of code that is running, and the stack of closures leads to many second order effects.

Problem 2: Error handling

Handling errors becomes difficult and very verbose. Swift 2 introduced an error handling model for synchronous code, but callback-based interfaces do not derive any benefit from it:

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

Problem 3: Conditional execution is hard and error-prone

Conditionally executing an asynchronous function is a huge pain. Perhaps the best approach is to write half of the code in a helper "continuation" closure that is conditionally executed, like this:

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

Problem 4: Many mistakes are easy to make

It's easy to bail out by simply returning without calling the appropriate block. When forgotten, the issue is very hard to debug:

func processImageData4(completionBlock: (result: Image?, error: Error?) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource, error in
        guard let dataResource = dataResource else {
            return // <- forgot to call the block
        }
        loadWebResource("imagedata.dat") { imageResource, error in
            guard let imageResource = imageResource else {
                return // <- forgot to call the block
            }
            ...
        }
    }
}

When you do not forget to call the block, you can still forget to return after that. Thankfully guard syntax protects against that to some degree, but it's not always relevant.

func processImageData5(recipient:Person, completionBlock: (result: Image?, error: Error?) -> Void) {
    if recipient.hasProfilePicture {
        if let image = recipient.profilePicture {
            completionBlock(image) // <- forgot to return after calling the block
        }
    }
    ...
}

Problem 5: Because completion handlers are awkward, too many APIs are defined synchronously

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.

Problem 6: Other "resumable" computations are awkward to define

The problems described above are on specific case of a general class of problems involving "resumable" computations. For example, if you want to write code that produces a list of squares of numbers, you might write something like this:

for i in 1...10 {
    print(i*i)
}

However, if you want to write this as a Swift sequence, you have to define this as something that incrementally produces values. There are multiple ways to do this (e.g. using AnyIterator, or the sequence(state:,next:) functions), but none of them approach the clarity and obviousness of the imperative form.

In contrast, languages that have generators allow you to write something more close to this:

func getSequence() -> AnySequence<Int> {
    let seq = sequence {
        for i in 1...10 {
            yield(i*i)
        }
    }
    return AnySequence(seq)
}

It is the responsibility of the compiler to transform the function into a form that incrementally produces values, by producing a state machine.

Proposed Solution: Coroutines

These problem have been faced in many systems and many languages, and the abstraction of coroutines is a standard way to address them. Without delving too much into theory, coroutines are an extension of basic functions that allow a function to return a value or be suspended. They can be used to implement generators, asynchronous models, and other capabilities - there is a large body of work on the theory, implementation, and optimization of them.

This proposal adds general coroutine support to Swift, biasing the nomenclature and terminology towards the most common use-case: defining and using asynchronous APIs, eliminating many of the problems working with completion handlers. The choice of terminology (async vs yields) is a bikeshed topic which needs to be addressed, but isn't pertinent to the core semantics of the model. See Alternate Syntax Options at the end for an exploration of syntactic options in this space.

It is important to understand up-front, that the proposed coroutine model does not interface with any particular concurrency primitives on the system: you can think of it as syntactic sugar for completion handlers. This means that the introduction of coroutines would not change the queues that completion handlers are called on, as happens in some other systems.

Async semantics

Today, function types can be normal or throwing. This proposal extends them to also be allowed to be async. These are all valid function types:

   (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.

Just as a normal function (#1) will implicitly convert to a throwing function (#2), an async function (#3) implicitly converts to a throwing async function (#4).

On the function declaration side of the things, you can declare a function as being asynchronous just as you declare it to be throwing, but use the async keyword:

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

// Semantically similar to this:
func processImageData(completionHandler: (result: Image) -> Void) { ... }

Calls to async functions can implicitly suspend the current coroutine. To make this apparent to maintainers of code, you are required to "mark" expressions that call async functions with the new await keyword (exactly analogously to how try is used to mark subexpressions that contain throwing calls). Putting these pieces together, the first example (from the pyramid of doom explanation, above) can be rewritten in a more natural way:

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
}

Under the hood, the compiler rewrites this code using nested closures like in example processImageData1 above. Note that every operation starts only after the previous one has completed, but each call site to an async function could suspend execution of the current function.

Finally, you are only allowed to invoke an async function from within another async function or closure. This follows the model of Swift 2 error handling, where you cannot call a throwing function unless you're in a throwing function or inside of a do/catch block.

Entering and leaving async code

In the common case, async code ought to be invoking other async code that has been dispatched by the framework the app is built on top of, but at some point, an async process needs to spawn from a controlling synchronous context, and the async process needs to be able to suspend itself and allow its continuation to be scheduled by the controlling context. We need a couple of primitives to enable entering and suspending an async context:

// 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<T>(
  _ 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<T>(
  _ body: (_ continuation: @escaping (T) -> (),
           _ error: @escaping (Error) -> ()) -> ()
) async throws -> T

These are similar to the "shift" and "reset" primitives of delimited continuations. These enable a non-async function to call an async function. For example, consider this @IBAction written with completion handlers:

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

These primitives enable callback-based APIs to be wrapped up as async coroutine APIs:

// Legacy callback-based API
func getStuff(completion: (Stuff) -> Void) { ... }

// Swift wrapper
func getStuff() async -> Stuff {
  return await suspendAsync { continuation in
    getStuff(completion: continuation)
  }
}

Functionality of concurrency libraries such as libdispatch and pthreads can also be presented in coroutine-friendly ways:

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

Generalized abstractions for coordinating coroutines can also be built. The simplest of these is a future, a value that represents a future value which may not be resolved yet. The exact design for a Future type is out of scope for this proposal (it should be its own follow-on proposal), but an example proof of concept could look like this:

class Future<T> {
  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)
      }
    }
  }
}

To reiterate, it is well known that this specific implementation has performance and API weaknesses, the point is merely to sketch how an abstraction like this could be built on top of async/await.

Futures allow parallel execution, by moving await from the call to the result when it is needed, and wrapping the parallel calls in individual Future objects:

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
}

In the above example, the first two operations will start one after another, and the unevaluated computations are wrapped into a Future value. This allows all of them to happen concurrently (in a way that need not be defined by the language or by the Future implementation), and the function will wait for completion of them before decoding the image. Note that await does not block flow of execution: if the value is not yet ready, execution of the current async function is suspended, and control flow passes to something higher up in the stack.

Other coordination abstractions such as Communicating Sequential Process channels or Concurrent ML events can also be developed as libraries for coordinating coroutines; their implementation is left as an exercise for the reader.

Conversion of imported Objective-C APIs

Full details are beyond the scope of this proposal, but it is important to enhance the importer to project Objective-C completion-handler based APIs into async forms. This is a transformation comparable to how NSError** functions are imported as throws functions. Having the importer do this means that many Cocoa APIs will be modernized en masse.

There are multiple possible designs for this with different tradeoffs. The maximally source compatible way to do this is to import completion handler-based APIs in two forms: both the completion handler and the async form. For example, given:

// 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;

The declarations above are imported both in their normal completion handler form, but also in their nicer async forms:

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

There are many details that should be defined as part of this importing process - for example:

  • What are the exact rules for the transformation?
  • Are multiple result functions common enough to handle automatically?
  • Would it be better to just import completion handler functions only as async in Swift 5 mode, forcing migration?
  • What should happen with the non-Void-returning completion handler functions (e.g. in URLSession)?
  • Should Void-returning methods that are commonly used to trigger asynchronous operations in response to events, such as IBAction methods, be imported as 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.

Interaction with existing features

This proposal dovetails naturally with existing language features in Swift, here are a few examples:

Error handling

Error handling syntax introduced in Swift 2 composes naturally with this asynchronous model.

// Could throw or be interrupted:
func processImageData() async throws -> Image

// Semantically similar to:
func processImageData(completionHandler: (result: Image?, error: Error?) -> Void)

Our example thus becomes (compare with the example processImageData2):

func loadWebResource(_ path: String) async throws -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) async throws -> Image
func dewarpAndCleanupImage(i: Image) async throws -> Vegetable

func processImageData2() async throws -> Image {
  let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  let imageTmp      = try await decodeImage(dataResource, imageResource)
  let imageResult   = try await dewarpAndCleanupImage(imageTmp)
  return imageResult
}

Coroutines address one of the major shortcomings of the Swift 2 error model, that it did not interoperate well with callback-oriented asynchronous APIs and required clumsy boilerplate to propagate errors across callback boundaries.

Closure type inference

Because the await keyword is used at all points where execution may be suspended, it is simple for the compiler to determine whether a closure is async or not: it is if the body includes an await. This works exactly the same way that the presence of try in a closure causes it to be inferred as a throwing closure. You can also explicitly mark a closure as async using the standard form of:

let myClosure = { () async -> () in ... }

defer and abandonment

Coroutines can be suspended, and while suspended, there is the potential for a coroutine's execution to be abandoned if all references to its continuation closure(s) are released without being executed:

/// Shut down the current coroutine and give its memory back to the
/// shareholders.
func abandon() async -> Never {
  await suspendAsync { _ = $0 }
}

It is to be expected that, upon abandonment, any references captured in wait by the continuation should be released, as with any closure. However, there may be other cleanup that must be guaranteed to occur. defer serves the general role of "guaranteed cleanup" in synchronous code, and it would be a natural extension to add the guarantee that defer-ed statements also execute as part of cleaning up an abandoned coroutine:

func processImageData() async throws -> Image {
  startProgressBar()
  defer {
    // This will be called when error is thrown, when all operations
    // complete and a result is returned, or when the coroutine is
    // abandoned. We don't want to leave the progress bar animating if
    // work has stopped.
    stopProgressBar()
  }

let dataResource  = try await loadWebResource("dataprofile.txt")
  let imageResource = try await loadWebResource("imagedata.dat")
  do {
    let imageTmp    = try await decodeImage(dataResource, imageResource)
  } catch _ as CorruptedImage {
    // Give up hope now.
    await abandon()
  }
  return try await dewarpAndCleanupImage(imageTmp)
}

This fills in another gap in the expressivity of callback-based APIs, where it is difficult to express cleanup code that must execute at some point regardless of whether the callback closure is really called. However, abandonment should not be taken as a fully-baked "cancellation" feature; if cancellation is important, it should continue to be implemented by the programmer where needed, and there are many standard patterns that can be applied. Particularly when coupled with error handling, common cancellation patterns become very elegant:

@IBAction func processImageData(sender: AnyObject) {
  beginAsync {
    do {
      let dataResource  = try await imageProcessor.loadWebResource("dataprofile.txt")
      let imageResource = try await imageProcessor.loadWebResource("imagedata.dat")
      let imageTmp      = try await imageProcessor.decodeImage(dataResource, imageResource)
      let imageResult   = try await imageProcessor.dewarpAndCleanupImage(imageTmp)
      display(imageResult)
    } catch CocoaError.userCancelled {
      // Ignore, user quit the kitchen.
    } catch {
      // Some really interesting error happened
      presentError(error)
    }
  }
}

@IBAction func stopImageProcessing(sender: AnyObject) {
  imageProcessor.cancel()
}

Internally, imageProcessor may use NSOperation or a custom cancelled flag. The intent of this section is to give a single example of how to approach this, not to define a normative or all-encompassing approach that should be used in all cases.

Completion handlers with multiple return values

Completion handler APIs may have multiple result arguments (not counting an error argument). These are naturally represented by tuple results in async functions:

// Before
func processImageHalves(completionHandler: (part1: Image?, part2: Image?, error: Error?) -> Void)

// After
func processImageHalves() async throws -> (Image, Image)

Source Compatibility

This is a generally additive feature, but it does take async and await as keywords, so it will break code that uses them as identifiers. This is expected to have very minor impact: the most pervasive use of async as an identifier occurs in code that works with dispatch queues, but fortunately keywords are allowed as qualified member names, so code like this doesn't need any change:

  myQueue.async { ... }

That said, there could be obscure cases that break. One example that occurs in the Swift testsuite is of the form:

extension DispatchQueue {
  func myThing() {
    async {
        ...
    }
  }
}

This can be addressed by changing the code to use self.async or backticks. The compiler should be able to detect a large number of these cases and produce a fixit.

Effect on ABI stability

This proposal does not change the ABI of any existing language features, but does introduce a new concept that adds to the ABI surface area, including a new mangling and calling convention.

Alternate Syntax Options

Here are a couple of syntax level changes to the proposal that are worth discussing, these don't fundamentally change the shape of the proposal.

Spelling of async keyword

Instead of spelling the function type modifier as async, it could be spelled as yields, since the functionality really is about coroutines, not about asynchrony by itself. The recommendation to use async/await biases towards making sure that the most common use case (asynchrony) uses industry standard terms. The other coroutine use cases would be much less common, at least according to the unscientific opinion of the proposal authors.

To give an idea of what this could look like, here's the example from above resyntaxed:

func loadWebResource(_ path: String) yields -> Resource
func decodeImage(_ r1: Resource, _ r2: Resource) yields -> Image
func dewarpAndCleanupImage(_ i : Image) yields -> Image

func processImageData1() yields -> Image {
  let dataResource  = yield loadWebResource("dataprofile.txt")
  let imageResource = yield loadWebResource("imagedata.dat")
  let imageTmp      = yield decodeImage(dataResource, imageResource)
  let imageResult   = yield dewarpAndCleanupImage(imageTmp)
  return imageResult
}

Make async be a subtype of throws instead of orthogonal to it

It would be a great simplification of the language model to make the async modifier on a function imply that the function is throwing, instead of making them orthogonal modifiers. From an intuitive perspective, this makes sense because many of the sorts of operations that are asynchronous (e.g. loading a resource, talking to the network, etc) can also fail. There is also precedent from many other systems that use async/await for this; for example, .NET Tasks and Javascript promises both combine error handling with async sequencing. One could argue that that's because .NET and Javascript's established runtimes both feature pervasive implicit exceptions; however, popular async frameworks for the Rust programming language, such as tokio.rs, have also chosen to incorporate error handling directly into their Future constructs, because doing so was found to be more practical and ergonomic than trying to compose theoretically-orthogonal Future<T> and Result<T> constructs.

If we made async a subtype of throws, then instead of four kinds of function type, we'd only have three:

    (Int) -> Int               // Normal function
    (Int) throws -> Int        // Throwing function
    (Int) async -> Int         // Asynchronous function, can also throw

The try marker could also be dropped from try await, because all awaits would be known to throw. For user code, you would never need the ugly async throws modifier stack.

A downside to doing this is that Cocoa in practice does have a number of completion handler APIs that do not take error arguments, and not having the ability to express that would make the importer potentially lose type information. Many of these APIs express failure in more limited ways, such as passing nil into the completion closure, passing in a BOOL to indicate success, or communicating status via side properties of the coordinating object; auditing for and recognizing all of these idioms would complicate the importer and slow the SDK modernization process. Even then, Swift subclassers overriding the async forms of these APIs would be allowed by the language to throw errors even though the error cannot really be communicated across the underlying Objective-C interface.

Make async default to throws

The other way to factor the complexity is to make it so that async functions default to throwing, but still allow non-throwing async functions to be expressed with nonthrowing (or some other spelling). This provides this model:

    (Int) -> Int                     // Normal function
    (Int) throws -> Int              // Throwing function
    (Int) async -> Int               // Asynchronous function, can also throw.
    (Int) async(nonthrowing) -> Int  // Asynchronous function, doesn't throw.

This model provides a ton of advantages: it is arguably the right defaults for the vast majority of clients (reducing boilerplate and syntactic noise), provides the ability for the importer and experts to get what they want. The only downside of is that it is a less obvious design than presenting two orthogonal axes, but in the opinion of the proposal authors, this is probably the right set of tradeoffs.

Behavior of beginAsync and suspendAsync operations

For async code to be able to interact with synchronous code, we need at least two primitive operations: one to enter a suspendable context, and another to suspend the current context and yield control back to the outer context. Aside from the obvious naming bikeshed, there are some other design details to consider. As proposed, beginAsync and continuation closures return Void to the calling context, but it may be desirable instead to have them return a value indicating whether the return was because of suspension or completion of the async task, e.g.:

/// Begin execution of `body`. Return `true` if it completes, or `false` if it
/// suspends.
func beginAsync(_ body: () async -> ()) -> Bool
/// Suspend execution of the current coroutine, passing the current continuation/// into `body` and then returning `false` to the controlling context
func suspendAsync<T>(_ body: (_ resume: (T) -> Bool) -> Void) async -> T

Instead of representing the continuation as a plain function value passed into the suspendAsync primitive, a specialized Continuation<T> type could be devised. Continuations are one-shot, and a nominal continuation type could statically enforce this by being a move-only type consumed by the resume operation. The continuation could also be returned by beginAsync or resuming a continuation instead of being passed into suspendAsync, which would put the responsibility for scheduling the continuation into the code that starts the coroutine instead of in the code that causes the suspension. There are tradeoffs to either approach.

Alternatives Considered

Include Future or other coordination abstractions in this proposal

This proposal does not formally propose a Future type, or any other coordination abstractions. There are many rational designs for futures, and a lot of experience working with them. On the other hand, there are also completely different coordination primitives that can be used with this coroutine design, and incorporating them into this proposal only makes it larger.

Furthermore, the shape and functionality of a future may also be affected by Swift's planned evolution. A Future type designed for Swift today would need to be a class, and therefore need to guard against potentially multithreaded access, races to fulfill or attempts to fulfill multiple times, and potentially unbounded queueing of awaiting coroutines on the shared future; however, the introduction of ownership and move-only types would allow us to express futures as a more efficient move-only type requiring exclusive ownership to be forwarded from the fulfilling task to the receiving task, avoiding the threading and queueing problems of a class-based approach, as seen in Rust's tokio.rs framework. tokio.rs and the C++ coroutine TR also both take the approach of making futures/continuations into templated/generic traits instead of a single concrete implementation, so that the compiler can deeply specialize and optimize state machines for composed async operations. tokio.rs and the C++ coroutine TR also both take the approach of making futures/continuations into templated/generic traits instead of a single concrete implementation, so that the compiler can deeply specialize and optimize state machines for composed async operations. Whether that is a good design for Swift as well needs further exploration.

Have async calls always return a Future

The most commonly cited alternative design is to follow the model of (e.g.) C#, where calls to async functions return a future (aka Task in C#), instead of futures being a library feature separable from the core language. Going this direction adds async/await to the language instead of adding a more general coroutine feature.

Despite this model being widely know, we believe that the proposed design is superior for a number of reasons:

  • Coroutines are generally useful language features beyond the domain of async/await. For example, building async/await into the compiler would require building generators in as well.
  • The proposed design eliminates the problem of calling an API (without knowing it is async) and getting a Future<T> back instead of the expected T result type. C# addresses this by suggesting that all async methods have their name be suffixed with Async, which is suboptimal.
  • By encoding async as a first-class part of function types, closure literals can also be transparently async by contextual type inference. In the future, mechanisms like rethrows can be extended to allow polymorphism over asynchrony for higher-level operations like map to work as expected without creating intermediate collections of Future<T>, although this proposal does not propose any such abstraction mechanisms in the short term.
  • The C# model for await is a unary prefix keyword, which does not compose well in the face of chaining. Wherein C# you may have to write something like x = await (await foo()).bar(), with the proposed design you can simply write x = await foo().bar() for the same reasons that you don't have to write try on every single call in a chain that can throw.
  • It is useful to be able to design and discuss futures as an independent standard library feature without tying the entire success or failure of coroutines as a language proposal to Future's existence.
  • There are multiple different interesting abstractions besides futures to consider. By putting the details of them in the standard library, other people can define and use their own abstractions where it makes sense.
  • Requiring a future object to be instantiated at every await point adds overhead. Since a major use case for this feature is to adapt existing Cocoa APIs, which already use callbacks, queues, target-action, or other mechanisms to coordinate the scheduling of the continuation of an async task, introducing a future into the mix would be an additional unnecessary middleman incurring overhead when wrapping these APIs, when in most cases there is already a direct consumer for the continuation point.
  • A design that directly surfaces a monadic type like Future as the result of an async computation heavily implies a compiler-driven coroutine transform, whereas this design is more implementation-agnostic. Compiler-transformed coroutines are a great compromise for integrating lightweight tasks into an existing runtime model that's already heavily callstack-dependent, or one aims to maintain efficient interop with C or other languages that heavily constrain the implementation model, and Swift definitely has both. It is conceivable that, in the eventual future, a platform such as Swift-on-the-server could provide a pure- or predominantly-Swift ABI where enough code is pure Swift to make cheap relocatable stacks the norm and overhead on C interop acceptable, as has happened with the Go runtime. This could make async a no-op at runtime, and perhaps allow us to consider eliminating the annotation altogether. The semantic presence of a future object between every layer of an async process would be an obstacle to the long-term efficiency of such a platform.

The primary argument for adding async/await (and then generators) to the language as first-class language features is that they are the vastly most common use-case of coroutines. In the author's opinion, the design as proposed gives something that works better than the C# model in practice, while also providing a more useful/general language model.

Have a generalized "do notation" for monadic types

Another approach to avoiding the one-true-future-type problem of C# could be to have a general language feature for chaining continuations through a monadic interface. Although this provides a more general language feature, it still has many of the shortcomings discussed above; it would still perform only a shallow transform of the current function body and introduce a temporary value at every point the coroutine is "awaited". Monads also compose poorly with each other, and require additional lifting and transformation logic to plumb through higher-order operations, which were some of the reasons we also chose not to base Swift's error handling model on sugar over Result types. Note that the delimited continuation primitives offered in this proposal are general purpose and can in fact be used to represent monadic unwrapping operations for types like Optional or Result:

func doOptional<T>(_ body: (_ unwrap: (T?) async -> T) async -> T?) -> T? {
  var result: T?

  func unwrap(_ value: T?) async -> T {
    if let value = value {
      return value
    }
    suspendAsync { _ in result = nil }
  }

  beginAsync {
    body(unwrap)
  }
}

Monads that represent repeated or nondeterministic operations would not be representable this way due to the one-shot constraint on continuations, but representing such computations as straight-line code in an imperative language with shared mutable state seems like a recipe for disaster to us.

Potential Future Directions

This proposal has been kept intentionally minimal, but there are many possible ways to expand this in the future. For example:

New Foundation, GCD, and Server APIs

Given the availability of convenient asynchrony in Swift, it would make sense to introduce new APIs to take advantage of it. Filesystem APIs are one example that would be great to see. The Swift on Server working group would also widely adopt these features. GCD could also provide new helpers for allowing () async -> Void coroutines to be enqueued, or for allowing a running coroutine to move its execution onto a different queue.

Documentation

As part of this introduction it makes sense to extend the Swift API design guidelines and other documentation to describe and encourage best practices in asynchronous API design.

rethrows could be generalized to support potentially async operations

The rethrows modifier exists in Swift to allow limited abstraction over function types by higher order functions. It would be possible to define a similar mechanism to allow abstraction over async operations as well. More generally, by modeling both throws and async as effects on function types, we can eventually provide common abstraction tools to abstract over both effects in protocols and generic code, simultaneously addressing the "can't have a Sequence that throws" and "can't have a Sequence that's async" kinds of limitations in the language today.

Blocking calls

Affordances could be added to better call blocking APIs from async functions and to hard wait for an async function to complete. There are significant tradeoffs and wide open design space to explore here, and none of it is necessary for the base proposal.

Fix queue-hopping Objective-C completion handlers

One unfortunate reality of the existing Cocoa stack is that many asynchronous methods are unclear about which queue they run the completion handler on. In fact, one of the top hits for implementing completion handlers on Stack Overflow includes this Objective-C code:

- (void)asynchronousTaskWithCompletion:(void (^)(void))completion;
{
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // Some long running task you want on another thread

    dispatch_async(dispatch_get_main_queue(), ^{
      if (completion) {
        completion();
      }
    });
  });
}

Note that it runs the completion handler on the main queue, not on the queue which it was invoked on. This disparity causes numerous problems for Cocoa programmers, who would probably defensively write the @IBAction above like this (or else face a possible race condition):

@IBAction func buttonDidClick(sender:AnyObject) {
  beginAsync {
    let image = await processImageData()
    // Do the update on the main thread/queue since it owns imageView.
    mainQ.async {
      imageView.image = image
    }
  }
}

This can be fixed in the Objective-C importer, which is going to be making thunks for the completion-handler functions anyway: the thunk could check to see if the completion handler is being run on a different queue than the function was invoked on, and if so, enqueue the completion handler on the original queue.

Thanks

Thanks to @oleganza for the original draft which influenced this!

@CrystDragon
Copy link

CrystDragon commented May 17, 2018

CMIIW, I believe in the following code snippet demonstrating coroutine-friendly queues:

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

It should be

sync ( continuation )
async ( continuation )

@CrystDragon
Copy link

CrystDragon commented May 17, 2018

@cloutiertyler
Me too! I'd love to see how generators can be built based on this new coroutine model.
And it surely is not a simple job, for it's related to another important aspect of the Swift language

https://github.com/apple/swift/blob/master/docs/OwnershipManifesto.md#expressing-mutating-and-non-mutating-iteration

@rnantes
Copy link

rnantes commented May 31, 2018

This would greatly improve swift, it's readability and the ease of compatibility across-platforms (linux)! Server-side frameworks such as Vapor desperately need a more straightforward structure for calling asynchronous code. I hope to be using this soon!

@bj97301
Copy link

bj97301 commented Jul 2, 2018

This would be nice. I hate callback pyramids.

@BroCode69
Copy link

RxSwift already solves most of the problems outlined in this proposal.

@aleczadikian
Copy link

aleczadikian commented Aug 23, 2018

Recently got through converting some Node.JS server code from callbacks to promises then from promises to async/await. Async/await is by far the best. It massively cleaned up my code. Can't believe this wasn't made standard long ago.

Maybe some don't like that it's rather "magical," hiding what's really going on from the programmer, but modern applications often follow a pattern falling exactly in line with what the async/await syntax is made for.

@fabb
Copy link

fabb commented Sep 26, 2018

Can't wait for this to be a part of Swift!

I can't await it either ;-)

@ryanmaxwell
Copy link

RxSwift already solves most of the problems outlined in this proposal.

Even if it did, wouldn’t you rather have Coroutines as a native and natural part of the language?

@edwinveger
Copy link

As someone who uses RxSwift extensively, I'd still love to see async/await become a native part of Swift. Although I couldn't imagine an iOS project without RxSwift, it is a big dependency (with a steep learning curve) and can sometimes confuse the compiler and the inexperienced reader alike.

I'm very fond of throws/try keywords and I think that async/await uses the same clarity in communicating intent/behavior to the reader. Completion blocks were one of the huge pains of Objective-C, and even though Swift does closures a lot better, this proposal uses sound and recognizable examples to demonstrating Swift's current shortcomings.
I'd like to commend the succinctness and focus of this proposal - good to keep Futures out of scope.

@yvbeek
Copy link

yvbeek commented Oct 31, 2018

Is this already an official proposal?

It would be great if this could become part of the Swift language.

What is the next step to take in this?

@dougzilla32
Copy link

dougzilla32 commented Nov 27, 2018

I love it!! I think this is an excellent solution to an annoying problem.

** UPDATE **

I have submitted a pitch on the Swift forum for adding cancellation abilities to async/await: https://forums.swift.org/t/proposal-to-add-cancellation-abilities-for-async-await/18419

The motivation is to provide a consistent and simple API to cancel asynchronous tasks.

And I've created a prototype project to demonstrate 'cancellation' and 'timeout' abilities for Async/Await: https://github.com/dougzilla32/AsyncCancellation

** ANOTHER UPDATE! **

4/10/19: I've updated both my fork of this document and my project to reflect the latest version of my Swift pitch for adding cancellation abilities to async/await. And I'm editing this post to bring it up to dates well. The latest proposed API from the Swift pitch is as follows:

Overview

In this proposal, the cancellation and timeout features are implemented using coroutine contexts:

  • A cancel scope (class CancelScope) is used to track cancellable asynchronous tasks within coroutines.
  • Cancellable tasks are manually added to the cancel scope as they are created, and are automatically removed from the cancel scope when their associated coroutine is resolved (i.e. the coroutine produces a result or an error).
  • The cancel scope has a cancel() method that can be used to explicitly cancel all unresolved tasks.
  • When cancel() is called on a cancel scope, all of its unresolved coroutines are immediately resolved with the error AsyncError.cancelled. Unwinding and cleanup for the associated task(s) happens in the background after the cancellation error is thrown.
  • The cancel scope is thread safe, therefore the same instance can be used in multiple calls to beginAsync
  • The cancel scope can produce subscopes for finer granularity of cancellation and timeouts.
  • The cancel scope has a timeout: TimeInterval initializer parameter for setting a timeout to cancel all unresolved tasks.

This proposal is influenced by Nathaniel J. Smith's excellent blog post Timeouts and cancellation for humans, which proposes cancel scopes as a human-friendly way to implement timeouts and cancellation. In our case, using a CancelScope instance as the beginAsync coroutine context sets up a cancellation scope.

Timer example

Here is a simple example using cancellation with async/await:

let cancelScope = CancelScope()
let error: (Error) -> () = { error in
    if error.isCancelled {
        print("Meaning Of Life calculation cancelled!")
    } else {
        print("An error occurred with the Meaning Of Life: \(error)")
    }
}

do {
    try beginAsync(context: cancelScope, error: error) {
        let theMeaningOfLife: Int = await
        suspendAsync { continuation, error in
            let workItem = DispatchWorkItem {
                Thread.sleep(forTimeInterval: 0.1)
                continuation(42)
            }
            DispatchQueue.global().async(execute: workItem)
            if let cancelScope: CancelScope = getCoroutineContext() {
                cancelScope.add(cancellable: workItem)
            }
        }
        if theMeaningOfLife == 42 {
            print("The Meaning Of Life is 42!!")
        } else {
            print("Wait, what?")
        }
    }
} catch {
    print(error)
}

// Set a timeout (seconds) to prevent hangs
cancelScope.timeout = 30.0

...

// Call 'cancel' to abort the request
cancelScope.cancel()

API

For 'cancellation' abilities the following changes and additions are proposed and are experimentally implemented in this project:

/// `Cancellable` tasks that conform to this protocol can be used with `CancelScope`
public protocol Cancellable {
    func cancel()

    var isCancelled: Bool { get }
}

/// Cancellation error
public enum AsyncError: Error {
    case cancelled
}

/**
 The 'CancelScope' serves to:

 - Track a set of cancellables, providing the ability to cancel the
   set from any thread at any time

 - Provides subscopes for finer grained control over cancellation
   scope

 - Provide the current list of cancellables, allowing extensions of
   'CancelScope' to invoke other methods by casting

 - Optionally specify a timeout for its associated cancellables

 Note: A cancellable is considered resolved if either its
 'continuation' or its 'error' closure has been invoked.
 */
public class CancelScope: Cancellable {
    /// Create a new `CancelScope` with optional timeout in seconds.
    /// All associated unresolved tasks will be cancelled after the
    /// given timeout. Default is no timeout.
    public init(timeout: TimeInterval = 0.0)

    /// Cancel all unresolved cancellables
    public func cancel()

    /// Returns true if all cancellables are either cancelled or
    /// resolved
    public var isCancelled: Bool { get }

    /// Add a cancellable to the cancel scope
    public func add(cancellable: Cancellable)

    /// The list of unresolved cancellables conforming to type 'T' for
    /// this cancel scope
    public func cancellables<T: Cancellable>() -> [T]

    /// Create a subscope.  The subscope can be cancelled separately
    /// from the parent scope. If the parent scope times out or is
    /// cancelled, all of it's  subscopes will be cancelled as well.
    /// The 'timeout' parameter specifies a timeout in seconds for
    /// the cancellation subscope, to cover the case where a shorter
    /// timeout than the parent scope is desired.
    public func makeSubscope(timeout: TimeInterval = 0.0) -> CancelScope
}

/**
 Return a coroutime context matching the given type 'T' by applying
 the following checks in sequential order:

 1. If the coroutine context matches type 'T', then it is returned

 2. If the coroutine context is an array, then return the first item
    in the array matching 'T'

 3. Return 'nil' if there are no matches
*/
public func getCoroutineContext<T>() -> T?

/**
 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.

 Calls to 'beginAsync' may be nested, which can be used to provide
 additional coroutine contexts.  Coroutine contexts from outer scopes
 are inherited by concantenating all contexts as an array.  The
 'getCoroutineContext' function runs through this array looking for a
 matching type.

 - Parameter asyncContext: the context to use for all encapsulated
   corotines

 - Parameter error: invoked if 'body' throws an error
 */
public func beginAsync(
    asyncContext: Any? = nil,
    error: ((Error) -> ())? = nil,
    _ body: @escaping () throws -> Void
) rethrows

/**
 Suspends the current coroutine and invokes 'body' with the
 coroutines'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.

 - Note: Cancellation is not supported with this flavor of
   'suspendAsync' and attempts to access the 'CancelScope' will
   trigger a fatal error.
*/
public func suspendAsync<T>(
    _ body: @escaping (_ continuation: @escaping (T) -> ()) -> ()
) -> T

/**
 Suspends the current coroutine and invokes 'body' with a continuation
 closure and a failure closure. The coroutine is resumed when either
 the continuation closure or the failure closure is invoked by 'body'.
 If 'body' invokes the continuation closure then 'suspendAsync' will
 return the provided value.  And if 'body' invokes the failure closure
 then 'suspendAsync' will throw the provided error.

 Only one of either the continuation closure or the failure closure
 may be called. It is a fatal error if both are called, or if either
 is called more than once.

 Code inside 'body' can support cancellation as follows:

     let cancellable: Cancellable = MyCancellableTask()

     ...

     // Add 'cancellable' to the 'CancelScope' coroutine context
     if let cancelScope: CancelScope = getCoroutineContext() {
       cancelScope.add(cancellable: cancellable)
     }
 */
public func suspendAsync<T>(
    _ body: @escaping (
        _ continuation: @escaping (T) -> (),
        _ error: @escaping (Error) -> ()
    ) -> ()
) throws -> T

This example shows how to define URLSession.dataTask as a coroutine that supports cancellation:

/// Extend 'URLSessionTask' to be 'Cancellable'
extension URLSessionTask: Cancellable {
    public var isCancelled: Bool {
        return state == .canceling || (error as NSError?)?.code
            == NSURLErrorCancelled
    }
}

/// Add `URLSessionTask` suspend and resume capabilities to
/// 'CancelScope'
extension CancelScope {
    var urlSessionTasks: [URLSessionTask] { return cancellables() }

    func suspendTasks() { urlSessionTasks.forEach { $0.suspend() } }

    func resumeTasks() { urlSessionTasks.forEach { $0.resume() } }
}

/// Add async version of dataTask(with:) which uses suspendAsync to
/// handle the callback
extension URLSession {
    func dataTask(with request: URLRequest) async
        -> (request: URLRequest, response: URLResponse, data: Data) {
        return await suspendAsync { continuation, error in
            let task =
            self.dataTask(with: request) { data, response, err in
                if let err = err {
                    error(err)
                } else if let response = response, let data = data {
                    continuation((request, response, data))
                }
            }
            if let cancelScope: CancelScope = getCoroutineContext() {
                cancelScope.add(cancellable: task)
            }
            task.resume()
        }
    }
}

This example demonstrates usage of the URLSessionTask.dataTask coroutine including cancellation and timeout

(from main.swift):

import Foundation

// Example: how to make a cancellable web request with the
// URLSession.dataTask coroutine
let cancelScope = CancelScope()
let error: (Error) -> () = { error in
    print("Apple search error: \(error)")
}

do {
    beginAsync(context: cancelScope, error: error) {
        let urlSession = URLSession(configuration: .default)
        let request = URLRequest(
            url: URL(string: "https://itunes.apple.com/search")!)
        let result = await urlSession.dataTask(with: request)
        let dataString = String(data: result.data, encoding: .utf8)
        print("Apple search result: \(dataString)")
    }
}
catch {
    print("Apple search error: \(error)")
}

// Set a timeout (seconds) to prevent hangs
cancelScope.timeout = 30.0

...

// Call 'cancel' to abort the request
cancelScope.cancel()

Here is the image loading example from the original Async/Await proposal, along with cancellation and timeout abilities

(from main.swift):

/// For the purpose of this example, send a simple web request rather
/// than loading actual image data
func loadWebResource(_ name: String) async -> Data {
    let urlSession = URLSession(configuration: .default)
    let request = URLRequest(
        url: URL(string: "https://mydatarepo.com/\(name)")!)
    let result = await urlSession.dataTask(with: request)
    return result.data
}

func decodeImage(_ profile: Data, _ data: Data) async -> Image

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

/// Image loading example
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(
        try dataResource.get(), try imageResource.get())
    let imageResult = await dewarpAndCleanupImage(imageTmp)
    return imageResult
}

// Execute the image loading example
let queue = DispatchQueue.global(qos: .default)
let cancelScope = CancelScope()
let error: (Error) -> () = { error in
    print("Image loading error: \(error)")
}

do {
    try beginAsync(context: [cancelScope, queue], error: error) {
        let result = try processImageData1a()
        print("Image result: \(result)")
    }
} catch {
    print("Image loading error: \(error)")
}

// Set a timeout (seconds) to prevent hangs
cancelScope.timeout = 30.0

...

// Call cancel to abort the request
cancelScope.cancel()

I'd love to get feedback and suggestions!!

@fabb
Copy link

fabb commented Jan 19, 2019

The type of the thrown error cannot be somehow defined like in Result, right? So a catching client needs to always handle a general Error additionally to an expected subtype, even if it can never happen.

@juepiezhongren
Copy link

juepiezhongren commented Feb 2, 2019

@elizarov, for C# world, coroutine-async is widely discussed, but it is still a common sense that future-async is a better way, esp for swift.
1)First of all, swift is with class and struct together, future is a class, but valueFuture should also be available, and all the result is extreme possibility for perf, high fault-tolerance or no alloc pressure or between.
2)Second, what makes sense for async/await is to make async/multiThreading programming looking close to sync way, where coroutine-async eg. golang it is just better than call-back hell.

@dougzilla32
Copy link

dougzilla32 commented Feb 9, 2019

FYI I have submitted a pitch on the Swift forum for adding cancellation abilities to async/await (see my full comments above): https://forums.swift.org/t/proposal-to-add-cancellation-abilities-for-async-await/18419

@dougzilla32
Copy link

Btw, I am in the process of contributing cancellation support for version 7 of the the popular PromiseKit framework for Swift. The 'Cancel' for PromiseKit pull request has been in the works for around 9 months.

My proposal is inspired by the PromiseKit work. I mention PromiseKit to give some confidence that my pitch has a basis in reality.

@hmlongco
Copy link

hmlongco commented May 1, 2019

Thought this might be a good place to mention that I discovered that it's possible to do a variant of async/await today in Swift 5 using the new Result type and GCD.

func load() {
    DispatchQueue.global(qos: .utility).async {

        let result = self.makeAPICall()
            .flatMap { self.anotherAPICall($0) }
            .flatMap { self.andAnotherAPICall($0) }
           
        DispatchQueue.main.async {
            switch result {
            case let .success(data):
                print(data)
            case let .failure(error):
                print(error)
            }
        }
    }
}

Where makeAPICall, anotherAPICall, and andAnotherAPICall are all implemented in the following fashion:

func makeAPICall() -> Result<String?, NetworkError> {

    let path = "https://jsonplaceholder.typicode.com/todos/1"
    guard let url = URL(string: path) else {
        return .failure(.url)
    }
    var result: Result<String?, NetworkError>!
    
    let semaphore = DispatchSemaphore(value: 0)

    URLSession.shared.dataTask(with: url) { (data, _, _) in
        if let data = data {
            result = .success(String(data: data, encoding: .utf8))
        } else {
            result = .failure(.server)
        }
        semaphore.signal()
    }.resume()

    _ = semaphore.wait(wallTimeout: .distantFuture)

    return result
}

The two keys to this approach lie in using DispatchSemaphore/wait/signal to get async/await assignment behaviors, and in using the new Result type's flatMap failure behavior to chain multiple API calls together with common error handling.

Without thread management, the load function above is roughly equal to the following async/await-style code:

func load() {
   do {
        let r1 = try await makeAPICall()
        let r2 = try await anotherAPICall(r1)
        let r3 = try await andAnotherAPICall(r2)
        print(r3)
     } catch {
       presentError(error)
     }
}

Though code-wise it's actually more similar to RxSwift's concept of flatMap event and error chaining, albeit done using pure Swift and GCD.

I wrote a Medium article that explains the technique in more detail.

@basememara
Copy link

I think the benefit of coroutines over locking mechanisms is they do not block the control flow and the overhead is low. In the semaphore example, the thread is stopped until those operations are completed one by one.

@iliaskarim
Copy link

Is this already an official proposal?

It would be great if this could become part of the Swift language.

What is the next step to take in this?

^^^

@RamblinWreck77
Copy link

That async/await implementation of the "pyramid of doom" is beautiful. We need this proposal shipped yesterday!

@chinsyo
Copy link

chinsyo commented Feb 1, 2020

This would be great!

@belozierov
Copy link

Hey guys, check out this Swift coroutines library - https://github.com/belozierov/SwiftCoroutine

@AverageHelper
Copy link

Hey guys, check out this Swift coroutines library - https://github.com/belozierov/SwiftCoroutine

I've been loving it so far! It's an interesting paradigm to play with. I've been using version 1.7 something. Not sure how his 2.0 version works just yet, but it's been getting almost constant patch updates for the last week. Fun to play with I suppose, but I'll wait until that calms down before I use it in production.

@larromba
Copy link

larromba commented May 31, 2020

Hey there. I'd love to see async / await introduced in Swift. In the meantime, I made a library adapted from a repo I saw using DispatchGroup. You can find the library here.

It allows you to write code like:

func foo() -> Async<String> {
    return Async { completion in
        async({
            let value1 = try await(self.someLongFunction(...))
            let value2 = try await(self.someLongFunction2(...))
            //...
            completion(.success("bar"))
        }, onError: { error in
            completion(.failure(error))
        })
    }
}

I've used it in a few projects and really liked it. If async / await ever gets officially released in Swift, hopefully it'll be an easy switch over. However, with the new Combine framework, maybe async / await isn't so necessary?

@iliaskarim
Copy link

Hey there. I'd love to see async / await introduced in Swift. In the meantime, I made a library adapted from a repo I saw using DispatchGroup. You can find the library here.

It allows you to write code like:

func foo() -> Async<String> {
    return Async { completion in
        async({
            let value1 = try await(self.someLongFunction(...))
            let value2 = try await(self.someLongFunction2(...))
            //...
            completion(.success("bar"))
        }, onError: { error in
            completion(.failure(error))
        })
    }
}

I've used it in a few projects and really liked it. If async / await ever gets officially released in Swift, hopefully it'll be an easy switch over. However, with the new Combine framework, maybe async / await isn't so necessary?

Good point. I also wrote an async task wrapper.

@anasanasanas
Copy link

what is the state of this proposal?

@realityworks
Copy link

Also bumping this, are we expecting this in Swift 6?

@lattner
Copy link
Author

lattner commented Sep 22, 2020

Yes, I'd expect something like it. You can read more about the plans for Swift 6 here.

@larromba
Copy link

Yes, I'd expect something like it. You can read more about the plans for Swift 6 here.

That would be great!

@realityworks
Copy link

Yes, I'd expect something like it. You can read more about the plans for Swift 6 here.

Thanks, with concurrency implemented well, we'd definitely pick Swift over C# for most development projects, particularly server and gaming.

@rstradling
Copy link

100% agree not going down the C# asyc function declarator pattern. Please keep async as a type as proposed. Coming from F# or Scala to C# where async is a decorator is annoying because it becomes turtles all the way down when you don't need turtles all the way down.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment