[swift-evolution] [Concurrency] async/await + actors
Mike Sanderson
m at mikesand.com
Fri Aug 18 19:16:16 CDT 2017
I can’t speak to the more low-level implications, but to the extent this is
essentially “syntactic sugar for completion handlers,” I can base my
opinion on making iOS apps in different contexts for about 5 years. (Long
enough to remember when Objective-C Blocks as completion handlers were the
great new thing that was going to perfectly solve all our problems). I
especially appreciated the clear goals section of the proposal, which I
thought was on-target.
I have some uncertainty about how async/await will work in
practice—literally, how to use it. Some of this is just needing to see many
more examples and examples in common scenarios at the site of use and in
the creation of async functions.
Most of the questions I had I answered myself in the course of writing this
up, but I include them for confirmation and to call attention to aspects
that may not be clear. (I also tend to use block and closure
interchangeably when it reads better, especially where I learned them as
objective-C completion blocks, please do correct if necessary.) I hope the
write up will provide some clarity for others (assuming it's basically
correct or correctly labeled where it may not be) and provide feedback on
where it could be more clear.
*Starting at beginAsync*
@IBAction func buttonDidClick(sender:AnyObject) {
// 1
beginAsync {
// 2
let image = await processImage()
imageView.image = image
}
// 3
}
After entering a beginAsync block, code will continue to execute inside the
block (like do blocks now) until it encounters an await-marked function. In
this sense it’s more beginAsyncContext not begin-being-asynchronous, which
is how it could be read. Although the end of this block will be where
synchronicity will resume once execution goes async (suspends?) inside it.
At the point of an await function, two things can happen: 1) If the
function needs time to execute -- if "it suspends" is how to describe it?
-- code execution will jump to outside the beginAsync block. 2) If the
function has its result already, code execution will continue inside the
block, or jump to catch error if it exists? (This would not be different
from now in that a receiver of a completion block can invoke it
immediately, before returning).
This is important to clarify because after the async block (or any
functions with completion blocks now) code afterwards can’t assume what
might have happened. But this proposal doesn’t solve the problem seen in
the @IBAction example, of order 1, 3, 2, which is actually worse because if
you can have immediate completion you aren’t sure if it is 1, 3, 2, or 1,
2, 3. This is actually an easy enough situation to handle if you’re aware
of it.
*Use of suspendAsync*
suspendAsync is the point at which the waiting is actually triggered? For a
while this threw me at first (no pun intended). Since it looks like the
more common transitive verb form of “suspend”, I read this as
“suspending-the-async”, therefore, resuming. The primitives looked like
beginAsync started doing something asynchronous and suspendAsync resumed
synchronous execution? Maybe the better order would be asyncSuspend (paired
with asyncBegin--or even better for that, asyncContext) would be less
likely to be confused this way? (More on the primitives just below)
However, even inside the block passed to suspendAsync the code is not
asynchronous yet. The code in the block passed to suspendAsync is still
executed at the time the block is passed in. That code is responsible for
taking the continuation block and error block, escaping with them and
storing them somewhere, and invoking either when the value or error is
ready. Is it correct that those blocks will be called on thread that
suspendAsync was called on?
It was also somewhat unclear what happens then the block passed to
suspendAsync reaches the end. Since suspendAsync is itself an async function
called with await, it looks like control now passes back to the end of the
original beginAsync block, wherever that is. That the getStuff() async wrapper
example returns the result of the call to suspendAsync in one line obscured
what was going on. That was
func getStuff() async -> Stuff {
return await suspendAsync { continuation in
getStuff(completion: continuation)
}
}
What's going on would be more clear over two lines. For example if we
wanted to do further processing after getting our async result before
returning:
func getStuff() async -> Stuff {
let rawStuff = await suspendAsync { continuation in
getStuff(completion: continuation)
}
return processed(rawStuff)
}
Where exactly execution is paused and will resume is more clear.
In fact, the show the full async/await life cycle, it’s possible to
demonstrate in the same scope before introducing the semantics of async
functions:
beginAsync {
do {
let stuff = try await suspendAsync { continuation, error in
//perform long-running task on other queue then call
continuation, error as appropriate
}
//Continuation block resumes here with `stuff`
doSomething(with: stuff)
} catch {
//error block resumes here
handleGettingStuffError(error)
}
}
This is correct? While as the comments state it may be true that eventually
many users won’t need to interact with suspendAsync (though I think
beginAsync will remain common, such as the @IBAction example) it’s familiar
the key method that breaks familiar procedural execution and creates the
blocks that will allow it to resume. During the transition it will be
especially important for those adapting their own or others’ code. It
should be prominent.
One opinion point that I do want to mention though about the last example:
There should probably be just one suspendAsync method, the one with an
error continuation. First, if there's the no-error version then that will
probably proliferate in examples (seen already) coders will get learn how
to use await/async ignoring errors, a habit we’ll have to break later. But
if there's only one suspend method, then await would always include try,
forcing programmers who want to ignore them to have an empty catch block
(or use the space for a comment with your legitimate reason!). An
empty/comment only catch block would also be the case for legacy APIs with
no completion error parameter, though maybe these could be imported as
throwing Error.nil or Error.false, etc. So all uses would look like beginAsync
{ … } catch { … }.
beginAsync {
let stuff = await suspendAsync { continuation, error in
//perform long-running task on other queue then call continuation,
error as appropriate
}
//Continuation block resumes here with `stuff`
doSomething(with: stuff)
} catch {
//error block resumes here
handleGettingStuffError(error)
}
I don’t lightly suggest trying to use syntax to force good habits, but in
this case it would be the cleaner API. The alternative is forcing code that
does handle errors to look like the last example above, obviously more
ungainly than code that ignores errors. Handling errors is already too easy
to ignore.
There are more substantive points that I want to touch on later, largely
from my use of a promise/future frameworks as part of a production app that
went really well, even when working with UIKit, AppDelegate,
NSNotification. (I also worked on a project where the last guy had rolled
his own promises/futures system; you can guess how that went.)
But I wanted to clarify the basics of use. Thanks for this proposal and
everyone’s comments.
Mike Sanderson
On Fri, Aug 18, 2017 at 5:09 PM, Adam Kemp via swift-evolution <
swift-evolution at swift.org> wrote:
> Thanks for the quick response!
>
> On Aug 18, 2017, at 1:15 PM, Chris Lattner <clattner at nondot.org> wrote:
>
> On Aug 18, 2017, at 12:34 PM, Adam Kemp <adam.kemp at apple.com> wrote:
>
> For instance, say you’re handling a button click, and you need to do a
> network request and then update the UI. In C# (using Xamarin.iOS as an
> example) you might write some code like this:
>
> private async void HandleButtonClick(object sender, EventArgs e) {
> var results = await GetStuffFromNetwork();
> UpdateUI(results);
> }
>
>
> This event handler is called on the UI thread, and the UpdateUI call must
> be done on the UI thread. The way async/await works in C# (by default) is
> that when your continuation is called it will be on the same
> synchronization context you started with. That means if you started on the
> UI thread you will resume on the UI thread. If you started on some thread
> pool then you will resume on that same thread pool.
>
>
> I completely agree, I would love to see this because it is the most easy
> to reason about, and is implied by the syntax. I consider this to be a
> follow-on to the basic async/await proposal - part of the Objective-C
> importer work, as described here:
> https://gist.github.com/lattner/429b9070918248274f25b714dcfc76
> 19#fix-queue-hopping-objective-c-completion-handlers
>
>
> Maybe I’m still missing something, but how does this help when you are
> interacting only with Swift code? If I were to write an asynchronous method
> in Swift then how could I do the same thing that you propose that the
> Objective-C importer do? That is, how do I write my function such that it
> calls back on the same queue?
>
> In my mind, if that requires any extra effort then it is already more
> error prone than what C# does.
>
>
> Another difference between the C# implementation and this proposal is the
> lack of futures. While I think it’s fair to be cautious about tying this
> proposal to any specific futures implementation or design, I feel like the
> value of tying it to some concept of futures was somewhat overlooked. For
> instance, in C# you could write a method with this signature:
>
> ...
>
>
> The benefit of connecting the async/await feature to the concept of
> futures is that you can mix and match this code freely. The current
> proposal doesn’t seem to allow this.
>
>
> The current proposal provides an underlying mechanism that you can build
> futures on, and gives an example. As shown, the experience using that
> futures API would work quite naturally and fit into Swift IMO.
>
>
> I feel like this is trading conceptual complexity in order to gain
> compiler simplicity. What I mean by that is that the feature feels harder
> to understand, and the benefit seems to be that this feature can be used
> more generally for other things. I’m not sure that’s a good tradeoff.
>
> The other approach, which is to build a specific async/await feature using
> compiler transformations, may be less generic (yield return would have to
> work differently), but it seems (to me) easier to understand how to use.
>
> For instance, this code (modified from the proposal):
>
> @IBAction func buttonDidClick(sender:AnyObject) {
> doSomethingOnMainThread();
> beginAsync {
> let image = await processImage()
> imageView.image = image
> }
> doSomethingElseOnMainThread();
> }
>
>
> Is less straightforward than this:
>
> @IBAction async func buttonDidClick(sender:AnyObject) {
> doSomethingOnMainThread();
> let imageTask = processImage()
> doSomethingElseOnMainThread();
> imageView.image = await imageTask
> }
>
>
> It’s clearer from reading of the second function what order things will
> run in. The code from the proposal has a block of code (the callback from
> beginAsync) that will run in part before the code that follows, but some of
> it will run after buttonDidClick returns. That’s confusing in the same way
> that callbacks in general are confusing. The way that async/await makes
> code clearer is by making it more WYSIWYG: the order you see the code
> written in is the order in which that code is run. The awaits just mark
> breaks.
>
> _______________________________________________
> 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/31c282f9/attachment.html>
More information about the swift-evolution
mailing list