SE-0304 (4th review): Structured Concurrency

This revision of the proposal is a HUGE step forward and addresses the significant concerns I had about previous drafts. All of the changes are major improvements. I am overall very supportive of this proposal, but have some remaining questions and an important suggestion for consideration (below).

Many thanks to the authors for their continued iteration on this bedrock proposal for Swift Concurrency.

This fits very nicely with Swift in general and is a key part of the new concurrency model. I am very excited and encouraged about this -- I think it will be a profound step and will cause ripples across the industry at large.

n/a

I've put a significant amount of effort into this proposal during the pitch phases, in the three prior iterations of the formal review, and with the Swift Concurrency effort in general. I read this draft in detail.


Unstructured tasks vs actor isolation

The Context Inheritance section (and the later Actor context propagation section) says this about launching unstructured tasks from within an actor context:

  • if executed within the scope of a specific actor function:
    • inherit the actor's execution context and run the task on its executor, rather than the global concurrent one,
    • the closure passed to Task {} becomes actor-isolated to that actor, allowing access to the actor-isolated state, including mutable properties and non-sendable values.

What does "specific actor function" mean in this case? Does that mean from within a method lexical defined on the actor type, or does this mean a function dynamically executing on an actor's executor?

If it is the former, this poses some potentially very surprising behavior, because refactoring a method in an actor out to a global function (or method on some other type) will cause a behavior change. This seems surprising and we should carefully consider this. If the refactored code would work naturally (because it is still on the current actors executor) then there is no concern here.


Implicitly eliminating references to self?

Related to the above question, Implicit "self" mentions this:

Closures passed to the Task initializer are not required to explicitly acknowledge capture of self with self. .... Note : The same applies to the closure passed to Task.detached and TaskGroup.addTask .

How is this implemented and why is special support required? Implicit vs explicit self has been widely discussed in the community and has pervasive impact on the language.

SE-0304 is a very large library proposal. I'd request that this syntactic sugar feature be split out to its own proposal, so we can see what the design space tradeoffs are, how the implementation works, and what it means for swift at large.


sleep nitpick

The Voluntary Suspension section lists:

We also offer an asynchronous sleep function, which accepts the number of nanoseconds to > suspend for:

 extension Task where Success == Never, Failure == Never {
    public static func sleep(nanoseconds duration: UInt64) async throws { ... }
 }

Thank you for adding the nanoseconds: label. I think this is well motivated and is a great step.

One further question: why is the duration a UInt64? Swift generally uses Int as the currency type for values like this, even those that are logically unsigned (e.g. the result of count). Shouldn't this be an Int?


Further naming concerns around launching tasks

The notes above mention:

Introducing a verb here is a huge step is a huge step forward, thank you!! However, I have a continued subtle but important concern about the naming applies for launching tasks that this proposal is using, and I still feel that an earlier version of the proposal had a better unifying approach.

To recap, this proposal provides three ways to launch a task:

  1. Structured task groups: group.addTask { ... }
  2. Unstructured attached tasks: let handle = Task { ... }
  3. Unstructured detached tasks: let handle = Task.detach { ... }

I have concerns with each of these, and a simple suggestion for how to resolve it:

  1. On the first, I am concerned that the verb in group.addTask { .. } is "add". This would make sense if TaskGroup were primarily a collection-like entity, but I don't think that is its primary function. I see it as an orchestrator of concurrent tasks which launches and tracks them (in an internal collection), managing and structuring their execution. The verb "add" is an implementation detail of how it maintains its internal collection, not the primary action that the client needs to think about.

  2. On the second, I am concerned that the proposal uses an unlabeled initializer on a value type -- but its primary operation is to provide a global side effect of launching a new task. This leads to some surprisingly subtle code, e.g. see this post which suggests the best way to “fire and forget” some work off to the Main actor is:

    Task { @MainActor in hello() }

    While anything can be taught, this doesn't scream "I'm launching a new task that runs independently of the current one" because there is no verb. Furthermore, as the proposal points out, this requires the extremely unconventional use of @discardableResult on an init, something that is completely unprecedented in the Swift standard library. This isn't a thing in Swift because the initializer for a value type like this usually defines the initial condition for its value -- we don't (ab)use types for C++-like RAII things.

  3. On the third, the verb detach doesn't convey the right action happening here and is inconsistent with other static members on Task. The other static methods on Task apply to the current task. This operation doesn't detach the current task, it is launching a new task "that is detached" from the current one.

Furthermore, these are all launching tasks, but there is no unifying theme here. Cocoa design patterns generally want similar operations to have common roots (e.g. "method families") which this approach doesn't have.

My suggestion is to make a very minor tweak to the proposal, standardize of the compound verb phrase "launchTask", and use global functions for each of these. This would give the following design:

  1. Structured task groups: group.launchTask { ... }
  2. Unstructured attached tasks: let handle = launchTask { ... }
  3. Unstructured detached tasks: let handle = launchDetachedTask { ... }

The simple fire and forget example above becomes:

launchTask { @MainActor in hello() }

which now has a verb in it. I think the introduction of a consistent verb pulls the family together and makes code using these operations much more clear. Of course, "launch" is just one suggestion, I would be equally happy with some other active verb (e.g. "run") applied consistently in this way.


Overall, this is a huge and important proposal which is a truly exciting step forward for Swift. The authors have put a tremendous amount of effort into fine tuning and refining this, and it really shows. Thank you to everyone working so hard on this!

-Chris

25 Likes