[swift-evolution] [Concurrency] A slightly different perspective

Nathan Gray n8gray at n8gray.org
Thu Aug 31 17:04:11 CDT 2017


I've been following the conversations around Chris Lattner's intriguing
async/await proposal and would like to offer my own take. I feel that the
proposal as written is almost perfect.  My suggestions for improving it are
not highly original -- I think they have all come up already -- but I'd
like to present them from my own perspective.

1. Fixing "queue confusion" *must* be part of this proposal.  The key bit
of "magic" offered by async/await over continuation passing is that you're
working in a single scope.  A single scope should execute on a single queue
unless the programmer explicitly requests otherwise.  Queue hopping is a
surprising problem in a single scope, and one that there's currently no
adequate solution for.

Also consider error handling.  In this code it's really hard to reason
about what queue the catch block will execute on!  And what about the defer
block?

```
startSpinner()
defer { stopSpinner() }
do {
  try await doSomeWorkOnSomeQ()
  try await doSomeMoreWorkOnSomeOtherQ()
} catch {
  // Where am I?
}
```

Please, please, PLEASE don't force us to litter our business logic with
gobs of explicit queue-hopping code!

2. The proposal should include some basic coordination mechanism.  The
argument against returning a Future every time `await` is called is
convincing, so my suggestion is to do it from `beginAsync`. The Future
returned should only be specified by protocol. The protocol can start with
minimal features -- perhaps just cancellation and progress.  There should
be a way for programmers to specify their own, more featureful, types. (The
proposal mentions the idea of returning a Bool, which is perhaps the
least-featureful Future type imaginable. :-)

To make this a little more concrete.  Let's look at how these proposals can
clean up Jakob Egger's excellent example of more realistic usage of async.
Here's the original "minimal" example that Jakob identified:

```
class ImageProcessingTask {
  var cancelled = false
  func process() async -> Image? { … }
}

var currentTask: ImageProcessingTask?

@IBAction func buttonDidClick(sender:AnyObject) {
  currentTask?.cancelled = true
  let task = ImageProcessingTask()
  currentTask = task
  beginAsync {
    guard let image = await task.process() else { return }
    DispatchQueue.main.async {
      guard task.cancelled == false else { return }
      imageView.image = image
    }
  }
}
```

Here's how it could look with the changes I'm proposing.  I will start with
the bare code to demonstrate the clarity, then add some comments to explain
some subtle points:

```
func processImage() async -> Image? { ... }
weak var currentTask: Future<Void>?

@IBAction func buttonDidClick(sender:AnyObject) {
    currentTask?.cancel()
    currentTask = beginAsync { task in
        guard let image = await processImage() else { return }
        if !task.isCancelled {
            imageView.image = image
        }
    }
}
```

IMHO this is a very clean expression of the intended behavior.  Each line
is business logic -- there is no extraneous code in service of the
execution model.  Here's the annotated version:

```
func processImage() async -> Image? { ... }
// If ARC can be taught to keep an executing future alive we can
// make this weak and not worry about cleanup.  See below.
weak var currentTask: Future<Void>?

@IBAction func buttonDidClick(sender:AnyObject) {
    currentTask?.cancel()
    // `beginAsync` creates a future, returns it, and passes it into
    // the closure's body. Can use beginAsync(MyCustomFutureType)
    // for your own future type
    currentTask = beginAsync { task in
        /* You can imagine there's implicitly code like this here
        task.retain();  defer { task.release() }
        */
        guard let image = await processImage() else { return }
        // Guaranteed back on the main queue here w/o boilerplate.
        if !task.isCancelled {
            imageView.image = image
        }
    }
}
```

Note that if `beginAsync` is spelled `async` (which I do like) this idea
dovetails nicely with the proposal to use `async` in place of `await` to
get "future-lite" behavior.  My personal belief is that there's no such
thing as "future-lite" and the ideas are largely equivalent.

This got pretty long so thanks for reading!  I look forward to hearing your
comments.

Thanks,
-n8

-- 
Functional Programmer, iOS Developer, Surfs Poorly
http://twitter.com/n8gray
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170831/414e619f/attachment.html>


More information about the swift-evolution mailing list