So is Task unstructured or what?

It’s my understanding that unstructured concurrency means actor inheritance is not guaranteed.

SE-0304 (Structured Concurrency)

This proposal only explicitly defines structured concurrency (async let, TaskGroup).

It does not explicitly say that Task {} does not inherit actor isolation.

It does say structured tasks inherit actor context, implying Task {} (which is unstructured) is different.

SE-0306 (Sendable and Actor Isolation)

This proposal explicitly states:

@Sendable closures are always non-isolated.

Since Task {} is not @Sendable by default, this leaves open the possibility that it may inherit actor isolation.

Apple Swift Concurrency Guide

The best direct quote about Task {} comes from Apple documentation:

Tasks automatically inherit the priority and task-local storage of the context they are created in. However, actor isolation depends on how the task is scheduled.

(Apple Swift Concurrency Docs)

This confirms actor inheritance is not guaranteed for Task {}.

Since there is no single place where Swift explicitly says Task does not inherit actor isolation, the conclusion is inferred from:

SE-0304 making a distinction between structured and unstructured tasks.

SE-0306 stating @Sendable closures do not inherit isolation.

Apple documentation saying actor inheritance in Task {} depends on scheduling.

Practical testing of Task {} in different contexts, which shows it does not always inherit actor isolation.

What We Know for Sure

  1. Structured tasks (async let, TaskGroup) always inherit actor isolation.

  2. Unstructured Task {} does not automatically inherit actor isolation, but it may.

  3. Task.detached {} never inherits actor isolation.

  4. If a closure inside Task {} is explicitly @Sendable, it will never inherit actor isolation.

What Is Not Explicitly Stated

There is no single document that explicitly states: Task {}` does not inherit actor isolation.

Instead, Apple’s documentation says it depends on scheduling, which implies it’s not guaranteed.

Instead of saying Task {} does not inherit actor isolation, the most precise way to state it is:

Task {} is unstructured concurrency, so it does not guarantee actor inheritance. It may inherit the surrounding actor context, but this depends on scheduling, and it is not something developers should rely on.

This aligns with:

SE-0304 (which differentiates structured/unstructured tasks).

SE-0306 (which confirms @Sendable closures are never actor-isolated).

Apple’s documentation (which explicitly states actor inheritance in Task {} depends on scheduling).

Would you agree this is a more precise way to phrase it?

1 Like

Confusingly, SE-0304 introduces us to "tasks" as a general unit of concurrency but then describes "structured tasks" and "unstructured tasks", only the last of which is named Task. But the fundamental properties are outlined in that proposal:

Unstructured tasks created with the Task initializer inherit important metadata information from the context in which it is created, including priority, task-local values, and actor isolation.

If called from the context of an existing task:

  • inherit the priority of the current task the synchronous function is executing on
  • inherit all task-local values by copying them to the new unstructured task
  • 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.

If called from a context that is not running inside a task:

  • consult the runtime and infer the best possible priority to use (e.g. by asking for current thread priority),
  • even though there is no Task to inherit task-local values from, check the fallback mechanism for any task-locals stored for the current synchronous context (this is discussed in depth in the SE-0311 proposal)
  • execute on the global concurrent executor and be non-isolated with regard to any actor.

Specifically, "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." You can also see this on Task.init itself:

public init(
  priority: TaskPriority? = nil,
  @_inheritActorContext @_implicitSelfCapture operation: sending @escaping @isolated(any) () async -> Success
)

@_inheritActorContext guarantees actor context is inherited, if one exists. This is not dependent on whatever you mean by "scheduling", it's determined statically at compile time.

5 Likes

The snippet @jon_Shier links defines Task’s inheritance very clearly. It is absolute guaranteed to behave like that.

This is exactly the opposite what is guaranteed.

Child tasks run concurrently to their creator and thus do not inherit isolation. Unless you start the closure with e.g. addTask { @MainActor in … }, then such child task is isolated to given actor.

While not super focused on this topic, I do recommend the recent talk on wwdc about structured concurrency as well: Beyond the basics of structured concurrency - WWDC23 - Videos - Apple Developer

1 Like

Yep. Confusing. Haha
But thanks for the clarification.

The real issue is the community at large not getting clear messaging on this.

1 Like

Not sure I fully agree with “very clearly”, but I see where you’re coming from. The WWDC talk you referenced is more about the basics, so it doesn’t quite address my specific question. Appreciate you taking the time to post, though!

Yeah you're right that documentation needs more work. It's something we're thinking about. Personally I'd really love to revamp all of our concurrency docs, I don't have concrete plans to share but it's something that's on our minds constantly.

Yes, that phrasing makes sense! Task {} doesn’t guarantee actor inheritance, but it might, depending on scheduling. It aligns well with SE-0304, SE-0306, and Apple’s docs. Developers should be cautious when relying on it.

Depending on surrounding source, there's nothing dynamic "depending on scheduling" about isolation; it is purely a compile time deterministic thing if you are isolated or not. If the Task{} started from an isolated context (method of an actor, something with isolated parameters it inherits that).

4 Likes

I'm not sure I'm understanding the conclusion of this thread.

Task {} doesn’t guarantee actor inheritance

Task {} guarantees actor inheritance

Which one is it? : S

Task {…} will “create an unstructured task that runs on the current actor.” This stands in contrast to Task.detached {…} which will “create an unstructured task that’s not part of the current actor.”

The above quotes are taken from The Swift Programming Language: Concurrency: Unstructured Concurrency:

The Task.init documentation reiterates this point:

This is absolutely consistent with the observations by @Jon_Shier. And @ktoso offered important caveats about async let and task groups. But you asked about Task {…}, specifically, and the answer is that this will inherit the actor context.

3 Likes

The first post you're quoting is simply incorrect.

You are talking about the claim that “Task {} doesn’t guarantee actor inheritance”? I would agree. That is not correct. When invoked from an actor-isolated context, Task {…} will inherit that context.