[Concurrency] Evolving the Concurrency design and proposals

Hello all,

We posted the first drafts of a number of proposals related to Swift concurrency a few weeks ago. At this time of this writing, the forum topics directly related to the concurrency proposals have more than 600 posts in total (!), which includes lots of great ideas for design improvements, requests for clarification, and so on. We'll have some revised drafts coming up shortly after the U.S. Thanksgiving break that will incorporate a lot of the feedback we've received, which we'll post as separate pitch threads.

In the interim, I'd like to summarize some of that feedback and what we'll be adjusting in the revised proposals. Think of this as an overall summary of various responses from the proposal authors and implementors that are now scattered throughout those 600+ posts. I won't capture all of the discussion, and each item here is intentionally light on detail to give the overall shape of the proposal's evolution. Details will be left to the revised proposals.

Organizational changes to the proposals

The first round of pitches was split across four different proposals (async/await, structured concurrency, actors, and Objective-C interoperability). Even these were large, and will grow as we add more detail, so we'll be splitting out more pieces into separate libraries:

  • The task library (currently part of the Structured Concurrency proposal) will become its own proposal.
  • Global actors (currently part of the Actors proposal) will become its own proposal.
  • Asynchronous handlers (@asyncHandler) weren't described anywhere, and will become their own proposal.

Specific changes to the proposals

Here are some of the specific changes we will be making for the second pitch:

  • Replacing the term "nursery" with "task groups" in the Structured Concurrency proposal.
  • Allowing a synchronous actor method to be executed from outside the actor as if it were async. (The current proposal says that only async methods can be invoked from outside the actor)
  • Banning overloading of async and synchronous methods that otherwise have the same signature (but see the below about overload resolution).
  • Removing the ability for an arbitrary class to conform to the Actor protocol.
  • Rejecting try await; we'll require await try to mirror the async throws ordering on declarations.
  • Clarify how concurrent data structures that handle their own locking internally fit into the picture.
  • Remove reference to @unmoveable attribute.

Important concerns that need further discussion

There are a number of important ongoing discussions about various aspects of the proposals. It is very likely that these discussions will result in changes to the proposal, but none have reached a state where we're ready to adopt specific new wording in the proposal. Some of the higher-impact ones include:

  • Actor "re-entrancy", where an async method of an actor can suspend and allow other code to execute on that actor, is a nuanced topic. It is likely that some change here will be required, which might involve (e.g.) adding syntax to change the behavior and/or changing the default behavior, but it needs more discussion.
  • Providing better actor-isolation guarantees for values passed across actors. The protocol-based actor isolation proposal (pitch #1, pitch #2) is one possible approach here.
  • Modeling more concurrency-related aspects in function types, such as "concurrent" and "actor-independent" function types.
  • Extending the actor-centric data isolation rules to local variables captured in closures.
  • Introducing reasync as a complement to rethrows.
  • Allowing an implicit conversion from synchronous functions to async functions.

Changes we are not inclined to make

There are a number of proposed changes that were considered and discussed, but which we are not inclined to make. For these, the revised proposals will provide rationale (generally as part of "Alternatives Considered").

  • We are not inclined to eliminate the overload-resolution rule that prefers async in asynchronous contexts and synchronous functions in synchronous contexts.

  • We are not inclined to replace actor class with actor.

  • We are not inclined to make await imply try.

  • We are not inclined to make async a function modifier (e.g., async func f()).

    Doug

48 Likes

Maybe also make it clear on how to create an async context? Which seems like a tremendous oversight? :)

All documents have focused on how async methods call each other and the usage of await, and the formally known Nurseries focused on grouping and spawning child async tasks?

But how do I create my first entry point for async-ness? E.g. instead of creating my non-async method taking a callback closure - some database loading function - I would like to make use of async - if I havenā€™t missed it somewhere it is not mentioned how I do this.

So as I said, feels like an oversight!

I might also be completely blind and/or Iā€™ve might have completely misunderstood something?

Thank you for all your hard work and otherwise excellent documents!

Edit: I asked about this earlier and @Lantua replied: [Concurrency] Structured concurrency - #44 by Lantua But feels like we did not get this covered completely?

Best regards
Alex

7 Likes

I also wrote some questions about XCTest testing which seemed appreciated but were ignored, @Ben_Cohen focused on just reading my adressed question to Apple - but ignoring my questions about testing and since XCTest is open source - so mu question seems relevant to the whole community.

So would be great with some answers about how we can XCTest async methods!

3 Likes

This sounds great, Doug! I wonder if you all could give some thought to structuring these proposals in clearer layers that build upon one another, so that we can evaluate them in smaller chunks? Being able to see all of them at once has been super-valuable in that it shows the overall vision and where it might lead, and demonstrates that even some of the highest-level ideas are well-developed and much more than mere hand-waving. I think the next round of revisions will make that sweeping view even more powerful.

That said, reviewing this much material at once is as stressful for the community as it is for the proposers, and the sense that the parts are tightly coupled makes adoption of the proposals seem riskier than it needs to be. A plan that clearly distinguishes fundamental parts from the higher-level parts that use them, and proposes to roll them out in order, would allow us to focus more intently on one part at a time, and reduce risk for both the language design and its development schedule.

Thanks,
Dave

15 Likes

If we ban overloading, then what situation does this overload-resolution rule apply to? Would this be Clang importer magic or am I misunderstanding the change? Would the overload-resolution rule interact with implicit promotion to async (it that is ever added)?

1 Like

Please see [Concurrency] Asynchronous functions - #140 by Douglas_Gregor

1 Like

That's Task.runDetached mentioned here https://github.com/DougGregor/swift-evolution/blob/structured-concurrency/proposals/nnnn-structured-concurrency.md#detached-tasks

This will be explained in more depth in the new separated out "Task library" proposal that Doug just mentioned here, we'll have more details there.

1 Like

This is a good idea. I think there is layering here, based on dependencies amongst the 7 proposals outlined. Hereā€™s an ordering for the ā€œcritical pathā€ in which one could tackle them:

  1. Asynchronous functions (async/await): the basis of everything else. This is modeling asynchrony, not adding concurrency.
  2. Structured concurrency: Adds concurrency on top of async/await.
  3. Actors: Data isolation via a new kind of class.
  4. Global actors: Extending actors across data types to also encapsulate (eg) global state, main thread, etc.

The Task library can come any time after structured concurrency.
Async handlers can come any time after actors.
Objective-C interoperability can come any time after async/await, although it will need splitting if actors arenā€™t mostly settled by then.

Thinking more about closure captures, there might be an opportunity to introduce some data isolation notions around those that actors would extend, rather than the other way around, which might simplify the presentation of actors.

Doug

31 Likes

This is really fantastic Doug. Thank you for the great summary - the revisions suggested seem like a great step forward.

-Chris

5 Likes

Hi,

If possible could the appropriate proposal also explain or mention how many actors an app can make or handle? Like with gcd it was first said that you could make an unlimited amount of queues but then later on you were advised against that.

I read about the unlimited amount when gcd first came out but only heard of the revised advice recently on this forum. So I have probably been misusing gcd for like a decade. I want to avoid that with actors.

Somebody else also asked this somewhere in those 600 posts. Maybe I missed the reply. Correct use is important though and this - limited vs ā€œunlimitedā€ - makes a big difference for an ordinary user in how actors should be used or thought about.

PS. These forums, pitches and discussions teach me a lot about programming. Itā€™s wonderful to follow even though I donā€™t understand everything. My background is physics not computer science. Appleā€™s documentation and Mike Ash his blog taught me a lot too back in the day. But well, if they contain ā€œbadā€ advice once in a while itā€™s difficult for a non-CS person to catch that. I have been looking forward to actors first since reading about them in Chrisā€™s proposal. His(?) phrase ā€œisland of serialization in a sea of concurrency" is what makes actors click for me.

My 2 cents as a random swift user.

4 Likes

The Task.runDetach Example is bad:

let dinnerHandle = Task.runDetached {
  await makeDinner()
}

Because the example itself calls another async method by using await. So the example fails to show us our first entry into the async world.

Edit:

The whole section about runDetached is also about tasks, not about pure asynchronous methods... Im not wondering about tasks - whether they are detached or not - Im wondering how I write my first async method that does not call another async method (doesnā€™t use await).

1 Like

I am assuming itā€™s async all the way down to main async.

Iā€™m having trouble understanding this. Can you expand on it please?

I think the inner-most async function would have to be on an Actor or use one of the Task APIs. Actors provide an execution context that can run concurrently. Even if an async function on an actor makes no awaited calls, there may there be a suspension before the function starts. I think Task will be built on builtins that can suspend (someone correct me of Iā€™m wrong...)
Edit Task.withUnsafeContinuation() is the API I was thinking of.

I think this is a reasonable question, because the async/await proposal fails to be clear on this point.

The implication of the proposal is that an async function must provide a suspension point. For that reason, not calling an async method ā€” or, rather, not using await in the body of the function ā€” is an advanced scenario, not (as you're maybe suggesting?) a simple scenario. In the simplest case, if you have nothing to await in that function, don't make it async.

If an async function doesn't have any await expressions or something else that serves as a suspension point, one of 2 things could happen:

  1. The compiler could give you an error message.
  2. The compiler could insert a suspension point, maybe just before the return.

I don't think this has been discussed in any of the other threads, so it would be nice if the proposal could be updated with something explicit about this.

1 Like

What would be the point of such function ? If it does call an async method, it doesn't have to be async itself.

If you're talking about writing your own async primitive, you still have to call runtime async functions like Task.withUnsafeContinuation, else you are not writing an async function.

  1. The function is a placeholder for which you haven't written any asynchronous guts for, yet.
  2. The function no longer needs to be asynchronous, but you can't change the public API for it.
  3. The function is only asynchronous on some paths through its code.
4 Likes

Yes Im talking about writing my own async primitive. Sounds like something we will be doing all the time..? So why is it so unclear how to do it?

Task.withUnsafeContinuation Huh? Why is the Task namespace involved? Why is static method call unsafe? What with continuation refer to exactly?

Why isnā€™t it:

Async.new

?

Or Async.create?

Because async is a facility to make function with completion callback usable like blocking functions, not to make blocking function magically asynchronous.

And it requires that the completion callback be called exactly once. As the system has no way to make sure this contract is enforced, it considers this call unsafe. Your primitive must be able to tell the runtime that the callback has been called, this is what continuation provides, a proxy object to notify the runtime.

Writing async primitive is considered advanced usage, and requires some knowledge about how async works under the hood.

What do you expect Async.create to do exactly ?

What Sajjon may be getting at, and what I'm now wondering myself, is how to wrap a closure based asynchronous function in a native Swift async function. This is something I'm doing all the time with promise/future implementations. But here, I have no explicit promise to fulfill:

func request() async -> SomeResult {
    requestWithClosure { result in
        // now what?
    }
}
2 Likes

This is exactly the use case of withUnsafeContinuation:

func request() async -> SomeResult {
    Task.withUnsafeContinuation { continuation in
        requestWithClosure { result in
            continuation.resume(returning: result) }
        }
    }
}

The unsafeness is unavoidable without extra language features to support the usage restrictions: continuations must be invoked exactly once and there must be no code executed after the continuation.

With such restrictions, it might be possible to provide a ā€œsafeā€ version that could be used in the simplest use cases, but see the section ā€œThe challenge with diagnostics for Unsafeā€ in Structured Concurrency.

3 Likes