# SE-0472: Starting tasks synchronously from caller context

**URL:** <https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883>\
**Category:** Proposal Reviews\
**Created:** [March 27, 2025, 4:04pm UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883 "2025-03-27T16:04:45Z")\
**Posts on this page:** 7\
**Page:** 4

<div class="post-metadata">

**Author:** ![John\_McCall](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/john_mccall/32/1351_2.png) [@John\_McCall](https://forums.swift.org/u/John_McCall)\
**Post date:** [April 15, 2025, 10:37pm UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/61 "2025-04-15T22:37:59Z")

</div>

> [@hborla](#):
>
> I personally think we should just embrace this as the behavior of `Task.startSynchronously` and change the name of the API to make its semantics more clear. The future direction section alludes to this alternative with an objection:

I agree that it is valuable to be able to call this from a context that happens to be dynamically isolated and still have it start synchronously. We may still want a warning for situations where this is statically impossible (or at least would only happen in improbable scenarios like two actors synchronizing), but we can let experience suggest whether that's actually important to diagnose. The feature strongly discourages this by using `@_inheritActorContext` to align the isolations, so in theory it requires the programmer to use a non-closure as the task function (or give the closure an explicitly different isolation) in order to see surprising behavior.

That said, I do think there's a real problem here with the current `@_inheritActorContext`, because `@_inheritActorContext` _doesn't_ inherit isolation if the outer context is isolated to a non-global actor which the closure doesn't capture. If I'm understanding correctly, this means that the closure will be inferred as non-isolated and therefore will never start immediately. We've talked elsewhere about whether this capture behavior is reasonable, but in this case I think it's clearly not, and we really ought to be forcing a capture so that we can inherit isolation properly.

---

<div class="post-metadata">

**Author:** ![ktoso](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/ktoso/32/44526_2.png) [@ktoso](https://forums.swift.org/u/ktoso)\
**Post date:** [April 16, 2025, 1:16am UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/62 "2025-04-16T01:16:09Z")

</div>

> [@John\_McCall](#):
>
> We may still want a warning for situations where this is statically impossible (or at least would only happen in improbable scenarios like two actors synchronizing), but we can let experience suggest whether that's actually important to diagnose.

So the signature in the proposal is specifically about the original pitched idea here: the "don't ever allow not synchronous execution", which is now being considered to change into "try to run synchronously". I was working out yesterday what the signature will have to become, and it seems it would be as follows:

```swift
@_implicitSelfCapture _ operation: __owned sending @isolated(any) @escaping () async throws -> Success

```

(which uncovered a but in interface printing, so we're working to fix that, while at it). We're not relying on the `@_inheritActorContext` here, but on comparing executor of current and target and doing the synchronous run if able to.

> [@John\_McCall](#):
>
> The feature strongly discourages this by using `@_inheritActorContext` to align the isolations, so in theory it requires the programmer to use a non-closure as the task function (or give the closure an explicitly different isolation) in order to see surprising behavior.

In practice this would be these specific situations where we definitely change isolation:

```swift
@SomeActor 
func test() {
  Task.<immediately?> { @OtherActor in } // always enqueues
}

actor X {
  func test() {
    Task.<immediately?> { @OtherActor in } // always enqueues
  }
}

```

Here we statically know these _must_ enqueue and won't be "immediate" / "start synchronous" _ever_. So I guess we could warn about it... I would not want to warn about situations where we're not 100% certain.

> [@John\_McCall](#):
>
> That said, I do think there's a real problem here with the current `@_inheritActorContext`, because `@_inheritActorContext` _doesn't_ inherit isolation if the outer context is isolated to a non-global actor which the closure doesn't capture. If I'm understanding correctly, this means that the closure will be inferred as non-isolated and therefore will never start immediately.

> [@John\_McCall](#):
>
> We've talked elsewhere about whether this capture behavior is reasonable, but in this case I think it's clearly not, and we really ought to be forcing a capture so that we can inherit isolation properly.

There's two parts to talk about here:

One is that we can actually "start synchronously if not actually closed over any specific isolation" but it's a bit of an implementation hack:

Basically, if we use `@isolated(any)` and the closure is not isolated, its executor will be generic, then we check if the "target" isolation is generic, and if so, we just start synchronously immediately on the caller thread. The subsequent isolation / executor of that closure though, e.g. after we suspend and resume would be the generic executor then though. So we'd have the same semantics as `Task{}` but we _would_ manage to start synchronously anyway. It's a bit of a hack since we interpret the "no specific isolation" as "ok to start on caller", but given that there's no way to enforce a closure to be nonisolated this actually kinda works I think.

I worked that out yesterday actually - you can [check the impl here](https://github.com/swiftlang/swift/pull/80821/files#diff-03ec3b0a524a8a45afffa59620ca14f8b89487743fe498b47a5a8513579e0c7aR2538) (pending a bug in `sending` checking).

The second topic is if that "forgot to close over isolated value / actor so therefore suddenly we execute on the global pool" is reasonable or not. Personally, I see a lot of people struggling with this and I'm more than sure that most developers don't realize this is the rule at all, so I'd love to fix it not only here but also for `Task{}` that "when created from context that is isolated, inherit that isolation".

IMHO, an ideal outcome would be that Task{} and this API would consistently just inherit the enclosing isolation and we'd be better off with less confusing language rule here overall.

---

<div class="post-metadata">

**Author:** ![ktoso](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/ktoso/32/44526_2.png) [@ktoso](https://forums.swift.org/u/ktoso)\
**Post date:** [April 16, 2025, 7:40am UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/63 "2025-04-16T07:40:09Z")

</div>

> [@mtsrodrigues](#):
>
> Kotlin has a `CoroutineStart.UNDISPATCHED` option on `launch` that seems to do the same thing:
> 
> [UNDISPATCHED | kotlinx.coroutines – Kotlin Programming Language](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-u-n-d-i-s-p-a-t-c-h-e-d/)

Overall just relying on setting a specific executor like this does effectively "execute using this \<...\>" can be nice, however Swift also has to take into account compile-time safety through isolation guarantees which Kotlin does not.

It would be very difficult to deal with e.g. an "options" parameter, where we'd get some opaque set of options passed to a function, then passed into `Task(options) {}` - we don't know what these options mean for isolation. Are we going to be caller isolated, just on global pool, or on some specific other actor..? It's impossible to know without checking at runtime what the options passed in were. That's why our design differs a bit from the usual just passing some options to methods - we also need to understand how such would change the execution semantics (and thus isolation) of the created task.

---

<div class="post-metadata">

**Author:** ![sliemeobn](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/sliemeobn/32/27698_2.png) [@sliemeobn](https://forums.swift.org/u/sliemeobn)\
**Post date:** [April 16, 2025, 8:59am UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/64 "2025-04-16T08:59:52Z")

</div>

> [@John\_McCall](#):
>
> Doing so would create re-entrancy where none exists today, potentially leading to all sorts of surprising behavior and possibly even deadlocks.
> 
> SE-0461 does not have the same problem; everything still runs in the exact same order from the perspective of the task.

I am not disagreeing, but I don't see why the pattern of

- add a module-wide compiler setting to change the default behavior
- have it opt-in for Swift 6
- add explicit factory functions for both versions (with less pressure to have concise naming of the future default)
- (maybe) one day change the default (have it opt-out) in a major version break

would not be possible here too.

The only tricky bit is how to conditionalize which specific Task-factory method to call based on a compiler setting - but messier problems have been solved before.

I do understand that the runtime-behavior changes here are vastly more ... let's say noticeable ... compared to isolation inheritance, but there are benefits on the other side of the equation too, eg:

- More deterministic transitions from sync to async code (people have resorted to wild things in testing setups to get that back)
- Possibility of a runtime that executes non-suspending async code almost without overhead
- Possibly better performance in general (it feels there is simply one less thing to do for most new Tasks)

---

<div class="post-metadata">

**Author:** ![hborla](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/hborla/32/27563_2.png) [@hborla](https://forums.swift.org/u/hborla)\
**Post date:** [April 16, 2025, 2:37pm UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/65 "2025-04-16T14:37:05Z")

</div>

> [@sliemeobn](#):
>
> I am not disagreeing, but I don't see why the pattern of
> 
> - add a module-wide compiler setting to change the default behavior
> - have it opt-in for Swift 6
> - add explicit factory functions for both versions (with less pressure to have concise naming of the future default)
> - (maybe) one day change the default (have it opt-out) in a major version break
> 
> would not be possible here too.

I think there are things we could do to make this feasible, e.g. we could allow upcoming features to be written in availability attributes so a new `Task.init` with the `startSynchronously` semantics is available when the feature is enabled and the old `Task.init` is unavailable. We could preserve the old behavior with a differently-named method, e.g. `Task.enqueue` and provide migration tooling that replaces all uses of `Task.init` with `Task.enqueue` to preserve semantics when enabling the upcoming feature, etc.

However, I don't think we should change the semantics of `Task.init` -- probably the most commonly used APIs in the concurrency library -- because I do not believe the current behavior of `Task.init` is actively harmful in the same way that the behavior of async functions is, and asking people to re-learn the behavior of `Task.init` has real consequences that I do not think are worth the benefits. I think the behavior of `Task.init` is more predictable because the enqueueing behavior is not based on a dynamic property, and like John mentioned, you have to be a bit more careful when using the `Task.startSynchronously` API. And the benefits you mention:

> [@sliemeobn](#):
>
> - Possibility of a runtime that executes non-suspending async code almost without overhead
> - Possibly better performance in general (it feels there is simply one less thing to do for most new Tasks)

Prioritize performance over ease-of-use, which is not the right default for `Task.init`.

I think `Task.startSynchronously` will be a very commonly used API. I do not think it will be more common than `Task.init` with its current semantics.

---

<div class="post-metadata">

**Author:** ![allevato](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/allevato/32/385_2.png) [@allevato](https://forums.swift.org/u/allevato)\
**Post date:** [April 16, 2025, 6:43pm UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/66 "2025-04-16T18:43:37Z")

</div>

Thanks for the discussion, everyone! This proposal has been [returned for revision](https://forums.swift.org/t/returned-for-revision-se-0472-starting-tasks-synchronously-from-caller-context/79311).

---

<div class="post-metadata">

**Author:** ![John\_McCall](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/john_mccall/32/1351_2.png) [@John\_McCall](https://forums.swift.org/u/John_McCall)\
**Post date:** [April 16, 2025, 9:02pm UTC](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883/67 "2025-04-16T21:02:17Z")

</div>

Closed on April 16, 2025, 9:02pm UTC



[Previous page](https://forums.swift.org/t/se-0472-starting-tasks-synchronously-from-caller-context/78883.md?page=3)
