I really can't comment on Apple products. The concurrency runtime support library is built as a separate library that builds upon existing APIs (e.g, libdispatch) that have been around for a while. If you grab the right PR and use a hack to start async code, it works fine on generally any macOS.
That's not the design of cancellation described in the Structured Concurrency proposal. A potential suspension point is not a return point. One manually checks for cancellation and can then choose to exit early via either of the normal mechanisms, whether it's return or throw. Cancellation is not a third kind of exit.
This is fine; lock.release() will always get called because await is a suspension point. It cannot return.
It's worth pointing out, though, that it may still be a different kind of bug because in some lock implementations the release must happen on the same thread as the acquisition. When you put the await in there then you may end up on a different thread, which may be a bug. That's another example of why it's important to see the suspension point so you might notice a bug like that.
That's not a flaw in the async/await proposal, IMO. It would be an argument for not providing a lock API that has such rules using separate acquire/release methods instead of a structured approach (like a method performWhileHoldingLock(_ fn: @autoclosure () -> Void). Then you couldn't possibly have an await in between the acquisition and release because that block is not async.
I see - very interesting! So once created, a coroutine’s detached stack can only be destroyed by resuming and running to completion? Well then, you can consider that concern as resolved.
I still think it is worth adding to the proposal as an important part of the programming model for await. Lots of implementations in other languages make suspension points implicit return points to support cancellation, IIUC.
I won’t pretend to know anything about designing a good lock API; the point was about objects which need some specific call between initialisation and destruction. It is straightforward to remember that throwing errors could aborting the function, but I think it would be much less obvious if await (which developers will associate with suspension) could do the same. Thankfully await cannot return from the function, so there’s no concern.
It’s quite interesting. Presumably it means we could use await inside synchronous non-escaping closures — if those closures are created in an async context (e.g. you should be able to await inside the Array and String unsafe initialisers, or inside Sequence’s map closure, if you call them from an async function).
I guess I’m still not understanding how async/await is intended to replace callbacks.
If I have a UI application where the user clicks a button to start downloading a file, then in today’s world the button-click function calls a download-file function which takes a completion handler. The button-click function then returns, and control goes back to the UI event loop.
In the proposed async world, where the download-file function is async, how does that work?
We can’t let an await suspend the UI event loop when the download-file function is called.
If I have to manually write Task.runDetached in all my button-click functions, that becomes onerous.
Does that mean all the button-click functions become async, whether or not they actually await anything, and the UI event loop calls Task.runDetached every time a button is clicked?
(Yes, I understand that Task.runDetached is part of another proposal. I am trying to understand how async and await from this proposal are intended to be used in the proposed world where all the concurrency features exist.)
I know it’s off topic for this thread, but I have asked for a more clear justification of this approach to cancellation and none has been provided thus far. Given that Structured Concurrency is a pitch and not an accepted proposal, I don’t think reviewers of async / await should be expected to accept its design as a given. There is more discussion to be had around cancellation before a final design is accepted.
My understanding is that cancellation is not a language feature (I.e., not built in to async/await at all). That makes it either up to the end developer or the standard library. It could be done by either returning early or by throwing.
The structured concurrency proposal is in part a standard library feature that includes mechanisms for handling cancellation more easily, including convenience utilities that make it easier to signal and check for cancellation. But you could also roll your own because it’s not tied to the language.
As proposed I think you would have to do something like that. In C# they have async void to cover this exact situation, but its use for anything other than event handlers is strongly discouraged. One C#-specific reason is that without a caller to await the result there is nowhere for exceptions to be handled, and so any exception thrown from a continuation (after the await) causes an immediate crash.
I think from the proposal authors’ perspective it just reduces the structuredness of the code, which makes some other things they want to do harder.
I do think it would be beneficial to have some kind of syntactic sugar for a use case like this, though, because it is so common. That is, some kind of way of marking a synchronous, non-throwing, void-returning function as “all of my code should be wrapped in a detached task”. The exception issue in C# doesn’t apply in that scenario, and I think this is going to be very common.
library: as you correctly point out, nothing magic about it and simply functions in the library which check and may act on cancellation
runtime: the Task abstractions and their parent/child relationships matter for how cancellation is propagated; e.g. cancelling a parent implies cancelling all its children; This piece is part of the core runtime.
The runtime bit is really part of the "Task" feature which we're planning to propose separately, and structured concurrency makes use of it.
Doug made a small diagram with all the proposal dependencies recently (though this will even keep growing a little bit ):
This will be the "Asynchronous Handlers" proposal. For functions which can be called from an sync context, but really are async "inside"; they must return void indeed. This will be it's own complete proposal as well. Short version: @asyncHandler func buttonClicked() { ... actually async here }, but let's shelfe that and get to it in the Asynchronous Handlers proposal soon.
(I know it's annoying with all these references to "this will be done in X", but the proposals are very large already so we're trying to keep them focused and one-by-one)
My point is that this is still a pitch and not an accepted proposal. I think there are very good reasons for cancellation to be implicit at every suspension point by default. This topic deserves a more robust debate than has occurred thus far, including a more detailed rationale for the proposed design. It should not be assumed for the sake of this review that the proposed design for cancellation will be the final accepted design.
The proposal here is up for review without cancellation. If you want to make a case for why it shouldn’t be accepted without a particular cancellation implementation/behavior then I think you will need to lay out why you think that’s the case. How do you think it should work?
This isn’t too dissimilar from my argument earlier that I don’t think this should be accepted unless certain other things from the other specs are accepted also. But in my case those things happen to already be part of those specs so I don’t have to work quite as hard to make a case for stopping progress.
I'm having difficulty understanding the underlying mechanisms of async/await, so would appreciate if someone could explain.
In the callback/completion world a context can have only one stack and there is only one way it can be unwound. Callbacks may or may not capture data (by copying) but they never mess with the stack.
In the async/await world though it is possible that something that we think of as a "context" may end up having multiple stacks, though they will be unwound synchronously within the given context (actor).
If my understanding is correct then it has some serious implications. Firstly, it should be clearly explained that async/await is not a "drop-in replacement" for callbacks even though for example the automatic translation of Objective-C API's might create an impression of that.
Second, if await captures my entire stack it means I need to rethink the way I solve problems. For example, I prepare data to be sent to a URL task, then I await for its completion with all the data stuck in memory while it's not necessary.
Third, does this all mean async/await with its stack switching come with a runtime overhead compared to callbacks? Can it be optimized to an extent that the developers would not need to worry about the efficiency when switching from callbacks to async/await? Because otherwise, you know, give me a good reason I need to convert other than readability and less boilerplate of course.
And finally, something as complex as say a URL task that can provide cancel, pause, resume and progress reporting can no longer be a simple async interface, it should in fact become a full actor with multiple entry points... and say callbacks for progress reporting?
Then what's the benefit of switching to the actor model (devil's advocate mode on) if the asynchronous callback interfaces are already simple and safe enough?
And a follow-up question: what's the equivalent of weak self in the async/await paradigm? Let's say a view controller is long gone from the screen but it's still awaiting with petentially a lot of unnecessary captures on the stack and potentially also unnecessary code that will be executed when the suspended function resumes. I guess it comes down to the unresolved problem of cancellation?
AsyncSequence may be a better fit for progress reporting in this case, I guess.
That's interesting and elegant, though it means I'm going to have at least two suspendable functions: one awaiting for completion and the other looping on the progress sequence. So two suspended stacks (partial tasks) vs. none in the callback model.
I'm really curious now how these old interfaces will be rewritten with the new concurrency paradigm. Especially curious about SwiftNIO because I have a suspicion that maybe Swift's actor/async model in its present form is not quite ready for time critical domains like high performace network servers, or as I mentioned in a separate post, in audio. Though right now it's difficult for me to be constructive without trying, examining the generated code and debugging it.
I feel like I'm missing something fundamental, but have been through the proposal a few times, and don't see what I am looking for.
First, I think this is a great direction for the language. Take existing patterns like completion blocks, and make them pretty. That's the way to go.
I also think the proposal does the following very well:
Explains what async/await is replacing. Ie why we want it
Explains the calling of async functions and blocks well
Explains how you compose an async function out of other async functions
But I'm missing a big one in the list: how do you actually create a general async function? I could see nothing there that was not implemented in terms of some other async function.
To make it concrete, if I have some existing call to CloudKit
(Methods like this are my bread and butter.) I want to make this an async func instead of having that ugly completion block. How do I do that?
It feels like I'm missing some keyword. Somehow that function would have to have an await of sorts after the database.add, but I am not aware of a standalone await, or a way that the fetchRecordZonesCompletionBlock could signal that it is finished so that the documentIdentifiersInCloud can return.
Is the idea that I should just block the return until the completion block has been called? Eg. using a dispatch group? That would not be such a clean solution IMO
My understanding is that, it would be either the underlying 3rd party API providing async'ness for you, or alternatively you create an actor which makes all its public entry points automatically async. Essentially your actor will be your new isolated thread executing stuff concurrently and consequently making all its external interaction async.
If I understand it, the only way to make a "leaf" async function is to use an actor(?) A third party library could introduce async functions, but presumably in the library itself it is also forced to work with actors to achieve this.
If there is no fundamental way to make an async function short of adopting actors, I get an uneasy feeling about the design. It feels like async/await should be usable without moving to actors. I would expect some way to make my own async functions. (Perhaps that was what some other people were getting at with the absence of generators etc.)