[swift-evolution] [Concurrency] async/await + actors
David Beck
david at davidbeck.co
Fri Aug 18 13:56:10 CDT 2017
https://gist.github.com/davbeck/e3b156d89b2e9d97bb5a61c59f8a07f7
# Async Await
I love this proposal so much. Much of it is exactly how I’ve thought
Swift’s concurrency model should look over the last year.
Making async a return attribute just like `throws` seems like the right
solution to me. Building on top of callbacks (rather than introducing
futures/promises) is also the right approach for swift. I think this
proposal nails the problem right on the head: callbacks don't work well
with the rest of Swift's error handling, is awkward, error prone, and yes,
looks ugly.
One point that I've gone back and forth on is how strictly to enforce
excecution order. For instance, in this example. it would make sense to
allow the first 2 lines to excecute in parallel and excecute the third line
once they both complete:
```swift
let a = await foo()
let b = await bar()
return [a, b]
```
But not in this case:
```swift
await client.connect()
let rooms = await client.getRooms()
```
In the first case the compiler could automatically optimize to run in
parallel, the second, it could not. I like the idea of wrapping parallel
code in a future, making the operation explicit and clear.
### Excecution context
I’m not familiar with C# or other implementations of async/await, only with
Javascript’s (very new) implementation. I’m curious how C# handles
execution contexts (threads, queue etc) since JS doesn’t have to deal with
that.
The `syncCoroutine` and `asyncCoroutine` example seems weird to me. It's
also unclear in that example what the context would be after a call to
async. Would excecution return to the queue, or be on whatever queue the
async function called back on? It makes a lot more sense to me to represent
this with code blocks, something like:
```swift
doSomeStuff()
await startAsync(mainQueue) {
doSomeStuffOnMainThread()
}
await startAsync(backgroundQueue) {
doSomeStuffInBackground()
}
```
Where every line in the code block is run on the context. This doesn't
handle synchronous excecution though. For instance, if we wanted to block a
queue until the entire async function had returned. An alternative might be
to have queues and other contexts define their own method that took async
functions:
```swift
doSomeStuff()
mainQueue.sync {
await loadSomethingStartingOnMain()
doSomeStuffOnMainThread()
// don't let anything else exceute on the main queue until this line
}
await mainQueue.async {
doSomeStuffInBackground()
}
```
Using `queue.async` taking an async function might be a good alternative to
a language "startAsync". Having the excecution context undefined based on
whatever queue the underlying code calls back on seems dangerous to me.
Forcing the user to define the context would fix that, but at the cost of
introducing extra dispatch calls where they may not be needed. A general
purpose context, that simply continued excecution in place would fix that,
and be more explicit when it was needed.
## Conversion of imported Objective-C APIs
One issue I see with the importer is that the conventions for callbacks
aren’t as strict as NSError ** methods were. For instance, URLSession
completion blocks include response, data and error, all of which are
optionals. The response is almost always present, even if there was an
error. But there is no way to know that from the ObjC type system, and no
way to represent a throwing function that also returns metadata on error in
Swift.
There are also cases where you wouldn’t want ObjC callbacks to be imported
as async functions. For instance, it wouldn’t make sense for
NotificationCenter callbacks to be awaited. In general, any callback that
can be called more than once is dangerous to use as an async function.
Personally, I would be in favor of taking a reserved but default on
approach to importing ObjC functions as async, and adding annotations to
ObjC to control their Swift importing. For instance, by default callbacks
with either 0 or 1 arguments would be imported as async non-throwing and
callbacks with 0 or 1 arguments plus an error would be imported as throwing
async. Callbacks with more than 1 argument would need to be manually
annotated. Methods that should not be async (like NotificationCenter) can
be annotated to not be imported as async.
Another issue we’ll need to contend with is intermediate tasks. Both
URLSession and the Photos framework come to mind. In the existing model,
they return something that allows you to cancel the request while it is in
progress. Consider the following example:
```swift
class ImageView {
private var currentTask: URLSessionTask?
var source: URL? {
didSet {
currentTask?.cancel()
image = nil
guard let source = self.source else { return }
load(source: source) { image, error in
guard self.source == source else { return }
if let image = image {
self.image = image
} else {
self.image = errorImage
}
}
}
}
var image: Image?
func load(source: URL, completion: @escaping (Image?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: source) { (data, response,
error) in
guard let data = data else {
completion(nil, error)
return
}
let image = UIImage(data: data)
completion(image, nil)
}
self.currentTask = task
task.resume()
}
}
```
How should we represent dataTask(with:completion:)? If I were writing this
API from scratch, task.resume() would be the async function, but that may
not be feasible for the importer.
If I could rewrite that example in a magical future version of Swift and
Foundation, it would look something like this:
```swift
class ImageView {
private var currentTask: URLSessionTask?
var source: URL? {
didSet {
currentTask?.cancel()
image = nil
guard let source = self.source else { return }
startAsync {
do {
let image = await try load(source: source)
guard self.source == source else { return }
self.image = image
} catch {
guard self.source == source else { return } // kind of awkward to have to
write this twice
self.image = errorImage
}
}
}
}
var image: Image?
func load(source: URL) async throws -> Image {
let task = URLSession.shared.dataTask(with: source)
self.currentTask = task
let data = await try task.resume()
return await UIImage(data: data)
}
}
```
## Actors
I fail to see the benefit of adding an entirely new construct for the actor
model. It seems like access control, value semantics, and dispatch queues
largely fill this need already, and the implimentation of an actor wouldn't
be so complicated currently that it needs a compiler feature to ensure a
correct implimentation.
Futher, it seems like giving up strict actor models would remove some of
the benefits other languages have with their actor models. It has been quit
a while since I worked with Erlang, but from what I remember, the magic of
it's recovery model comes from it's recursive, functional model. It can
recover from otherwise fatal errors because it can just reset it's state to
the state before the last message was received. For reasons outlined
already, Swift can't enforce that strictly, so no matter what we are going
to have to rely on implimentations to be implimented correctly.
Perhaps I'm missing something though.
On Thu, Aug 17, 2017 at 3:25 PM, Chris Lattner via swift-evolution <
swift-evolution at swift.org> wrote:
>
> On Aug 17, 2017, at 3:24 PM, Chris Lattner <clattner at nondot.org> wrote:
>
> Hi all,
>
> As Ted mentioned in his email, it is great to finally kick off discussions
> for what concurrency should look like in Swift. This will surely be an
> epic multi-year journey, but it is more important to find the right design
> than to get there fast.
>
> I’ve been advocating for a specific model involving async/await and actors
> for many years now. Handwaving only goes so far, so some folks asked me to
> write them down to make the discussion more helpful and concrete. While I
> hope these ideas help push the discussion on concurrency forward, this
> isn’t in any way meant to cut off other directions: in fact I hope it helps
> give proponents of other designs a model to follow: a discussion giving
> extensive rationale, combined with the long term story arc to show that the
> features fit together.
>
> Anyway, here is the document, I hope it is useful, and I’d love to hear
> comments and suggestions for improvement:
> https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782
>
>
> Oh, also, one relatively short term piece of this model is a proposal for
> adding an async/await model to Swift (in the form of general coroutine
> support). Joe Groff and I wrote up a proposal for this, here:
> https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619
>
> and I have a PR with the first half of the implementation here:
> https://github.com/apple/swift/pull/11501
>
> The piece that is missing is code generation support.
>
> -Chris
>
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170818/43d303d4/attachment.html>
More information about the swift-evolution
mailing list