SE-0304 (2nd review): Structured Concurrency

While we're bike-shedding, I find "spawning a child (task)" to sound pretty weird, almost like an impedance mismatch.


Like I said, I’m still not sure I understand the proposal fully. Can you know ahead of time whether a function will run on a serial or concurrent executor? Don’t you have assume that any task group could potentially execute subtasks concurrently?

If we’re going to have a low level syntax and a “lightweight” syntax it would be helpful to have an example of the type of pattern you see as impossible within the design space of the lightweight syntax. Spawn let looks great, and I wonder if the basic idea of spawning within a scope could cover all the bases with some design exploration.

You can use this handy list of proposals: Swift Concurrency Proposals · GitHub

To locate: swift-evolution/ at async-let · jckarter/swift-evolution · GitHub which was split out from the structured concurrency proposal some time ago because people found it distracting. That version is a bit outdated but the general idea stands.

As I've just completed some changes in implementation of those and we have now a better understanding of the feature in general, the proposal will get a refresh and more details very soon -- and then it's own review.

It's a bit catch-22, as we moved the async let proposal out of this proposal based on feedback; and now feedback is that we need to discuss the async let etc. at the same time :wink: So... yes, there will be some form of spawn let how we intend to call it, and very likely other things as well, so it is just just pure speculation on my part about future keywords but anticipation of where we're going.

1 Like

It's the same as spawning a thread; I don't think we're at this point willing to bike shed the word spawn any longer; the core team has debated this for a long time and decided we're going with spawn.


Some quick feedback on the proposal, I apologize for being after the official review cycle:

  • I love the improvements from v1 of the proposal. Moving things out of Task is much more clear.
  • I agree with other comments that the priority names should be generalized, even if they remain a small number of integers. This proposal seems reasonable to me.
  • I think that standardizing around the term spawn is a good way to go, but I'd recommend going further and pulling it into the detatch method. Instead of:
let dinnerHandle = detach {
  try await makeDinner()

I think it would be more fluent and would avoid introducing another top-level verb if we used:

let dinnerHandle = spawnDetached {
  try await makeDinner()

It seems ok to make the unstructured concurrency API more verbose since we prefer structured concurrency. This also maintains alignment around the spawn verb. Another alternative is detatchTask, but I think that aligning with spawn is a better way to go. You could go really crazy and use spawnDetatchedTask I suppose.

  • With the introduction of effectful properties, should get() in let dinner = try await dinnerHandle.get() be an async get-only property, e.g. let dinner = try await dinnerHandle.value? The corresponding getResult() would just be .result. This aligns with the general cocoa naming convention which doesn't like "get" methods.

  • On withTaskCancellationHandler I agree that putting the cancelation handler second and using something like onCancel seems most consistent with other frameworks and with do/catch syntax.

Overall, this is really great work, I'm excited to see it coming into Swift!



I’m not an expert in async tasks, but I’ve been using Swift for a long time and one thing I’m pretty sure of is that Swift’s idiom for creating a new instance is to call an initializer.

In this case, we are creating a new task, so the spelling most in line with Swift’s existing standards is:

let dinnerTask = Task {
  try await makeDinner()

We are creating a Task, so we should call an initializer on a type named Task.

I understand that the current design calls this type TaskHandle, but that does not align with Swift’s conventions.

When you create a string, you get a String not a StringHandle. When you create an array, you get an Array not an ArrayHandle. And when you create a task, you should get a Task not a TaskHandle.

The type which the proposal calls Task, based on how I see it used in the proposal, would be better spelled CurrentTask.


Nice! That would be awesome.

It would be nice to change Result.get() too, then :+1: But that's of course a bigger change.

After looking at the Task-Local Value proposal, I also believe that UnsafeCurrentTask doesn't really benefit anyone outside of the standard library. It may have more functionalities than the Task object, but we've been adding them as the needs arise. So an outsider would just use other (safe) APIs within the standard library. Unless the reason is performance, which I don't think is the case.


It's indeed quite apparent that there's a blatant gap in spawning a group of heterogeneous child tasks, especially in the first example of Task groups and child tasks. Not that I particularly mind.

Maybe "spawn a subtask"? I feel like the term child task only works well with nursery terminologies, from which we've since moved away. We still have "parent task" though, :thinking:. Oh well, let bikeshed be bikeshed.


I prefer detach because I assume that this function may be called pretty often.

I truly hope we don't start adding language complexity to start working around existing problems with the language (e.g. lack of variadic generics, and lack of trivial escape checking). I don't see any reason why the makeDinner example couldn't be sugared with /library features/. While it would be best with variadic generics, it would also be perfectly possible to have a bunch of overloads that would allow it to be expressed as something like this:

func makeDinner() async -> Meal {
  // Could also declare these separately and use destructuring instead of initialization.
  // var veggies: [Vegetable], meat: Meat,  oven: Oven

  // Run three closures in parallel using a library feature, returning when all return.
  var (veggies, meat, oven) = try await runInParallel({
      try await chopVegetables()
    }, {
      await marinateMeat()
    }, {
      await .oven(preheatOven(temperature: 350))

  // No need for force unwraps anymore.
  let dish = Dish(ingredients: [veggies, meat])
  return try await oven.cook(dish, duration: .hours(3))

Such APIs can be built on top of the basic task group API. Such APIs could also have forms that return TaskHandles, and would be more akin to the proposed async let thing. There is a wide range of convenience APIs for this, just as there are things like parallelForEach/parallelMap that should be part of the standard library, even though they aren't part of the primitive Task APIs.

The only utility that async let sugar provides is more safety around "value does not escape" checking. Such checking would be just as useful for many many other values in Swift (including the group argument to the closure in withTaskGroup, the argument to withUnsafeCurrentTask, withUnsafeBufferPointer, .....etc). Paving this out as part of the general language would be far more valuable than one tiny part of the language/library, and would not require introducing full ownership to get a lot of incremental safety.



Speaking personally, I'd very much prefer to stick with detach than to add needless additional words. I think salting a legitimate and useful function like this is unnecessary (and doing so because "we prefer structured concurrency" is excessive moralizing).

Additionally, the whole win with "spawn" is its link with parent/child relationships. But detached tasks are not children, so the term "spawn" is inappropriate, rather than synergistic.


I can see where you are coming from, but I see it differently.

The point of “spawn” was to align with concurrency terminology, where its use is pervasive. That prior art isn’t generally “structured”, so using it with detach seems very reasonable to me. Furthermore, even Ignoring history, I don’t see a “structured” connotation to the verb spawn.

My argument against detach as a top level function/word/command isn’t about moralizing, it is about simplifying and standardizing the lexicon around concurrency. Neither “spawn” or “detached” is a needless word. Detach without a qualifier is too ambiguous, and “detach” is not a concurrency term of art like spawn is.



Apologies for being way after the official review deadline.

I followed the 1st pitch, but not later pitches and the 1st review.

In my opinion, structured concurrency is perhaps the most straightforward piece of the concurrency effort.

My feedback focuses on API back-shedding, and echos some posts up-thread.

Although the term "nursery" is much disliked by most, I find it convey better than "task group" the sense that it's where children are kept alive. Perhaps withTaskGroup can be renamed as withChildTaskGroup? This way it conveys to the user directly that it's a group of child tasks, not just some arbitrary tasks. Also it is more consistent with the function's generic type parameter ChildTaskResult.

What about .successfulResult instead of .value? .value is much better than .get(), but it still feels not very obvious what the return is.

I'm also in favor of single verbs, and specifically: spawn (always child task), detach always NOT-child task, because it is simpler to reason and teach about:

  • A: okey how do I make some tasks?
  • B: you use spawn (covers group.spawn, the upcoming spawn let)
  • A: okey I know about spawn; what is detach?
  • B: Oh yeah don't detach, that's terrible, unless [...]

Whereas if both were "spawn" and "spawn but with some caveats, try not to use it". it becomes conflated in discussions and I can see this happening:

  • A: so I spawned a task but it didn't get cancelled, why?
  • B: how do you spawn?
  • A: spawnDetached, only thing the compiler would let me use.
  • B: Oh yeah never spawn detached, [explanation follows ...]

So IMHO by using a completely different verb we make it clear that it's not "some spawn that the compiler will accept here" but a different thing I need to separately learn about, even if I then in my head learn "okey, so like spawn but not a child-task".


I also still think there might be the need for one other verb, similar to send or something... where we want to not really detach but we're in a synchronous function and must call an async function; so we make a new task, but it inherits both execution context, priority, task-locals and everything else it can. With those three I think we'd have covered all use cases... The send I don't know if everyone agrees with, but pretty sure we'll need something for "sync calling async" that is better than detach.


I–personally–really dislike that await x.get() pattern and secretly wish for a re-throwing protocol like this:

protocol Awaitable { 
  associatedtype T: Sendable
  func get() async rethrows -> T (?) 

and writing:

await x // equivalent to await x.get()

but so far it hasn't been critical to introduce this sugar. Partially it was argued that "detach (and therefore Task.Handle) is bad and should be ugly" but maybe this is a thing to consider still?

To be honest this feels like one of those things we can add later once we gained more experience and know how often handles really do show up.

// This of course is inspired by how .NET has it: Asynchronous Operations: Awaitables though the exact semantics are just slight sugar for the get for us.


The connotation is not to "structured" but to "children". Presumably because of the dictionary definition of the word, spawn as a term of art is tied to creating child processes specifically. So adding it to detached tasks, which are not child tasks in the swift sense, would not just be unnecessarily verbose, but also incorrect.

It's maybe library-specific rather than an industry term of art, but detached does have an existing meaning in Grand Central Dispatch which is similar to the meaning proposed for Swift, which is to dissociate the work item's attributes from the current execution context.


I apologize for not having gotten to this review until way after the review period is up; I have other thoughts on it which, if it goes to another review, I would love to share.

However, on this point specifically, I have been incubating a thought for a while: The word that comes to mind which goes with spawn to indicate a "bunch" of things spawned is: brood. I wonder if it would be feasible even to use a result builder here so that the use site reads:

brood(CookingStep.self) {
  spawn { try await .vegetables(chopVegetables()) }
  spawn { try await .meat(marinateMeat()) }
  spawn { try await .oven(preheatOven(temperature: 350)) }

I agree with authors' choices in preferring detached to a compound word that includes spawn: I think it's nice to separate the word we use for the "attached" (structured) operation from that for the detached operation, whatever the strength of precedence for a term-of-art.


It's maybe library-specific rather than an industry term of art, but detached does have an existing meaning in Grand Central Dispatch which is similar to the meaning proposed for Swift, which is to dissociate the work item's attributes from the current execution context.

Yes, detach has a long history predating GCD with at least a decade+, used in at least e.g. Solaris threads with THR_DETACHED (1992) and in pthreads with pthread_detach and the PTHREAD_CREATE_DETACHED thread creation attribute (1995).

So I would say it's an established concurrency term/concept since quite a while back.


Hello everyone,
I'd like to share some performance findings and implications for this proposal.

We are trying very hard to optimize tasks such that for async let (soon to be known as spawn let) we'd be able to allocate the entire task object using task-local allocation (!) This will be a very noticeable performance gains for small child tasks created by async let declarations. These will potentially not have to malloc at all, and also don't need to be ref-counted either, both of which should cause noticeable performance gains.

This implies, that we must not allow arbitrary code to keep references to tasks around, resulting in the removal of Task.current from this proposal and APIs.

The API to "get" the task is not really necessary due to the existence of the static isCancelled, currentPriority properties, and task-locals being implemented as <taskLocal>.get().

This also reduces the duplication of APIs, there no longer are three ways to ask about isCancelled, but only one primary: Task.isCancelled, and one unsafe currentUnsafeTask.isCancelled.

The withUnsafeCurrentTask { (task: UnsafeCurrenTask?) in } API remains, and if used carefully is perfectly fine, but one must be very careful with not storing the task, as usual with all those withUnsafe... APIs.