# Calling non-isolated async method from a child task inheriting global actor allows for a race condition in Swift 6

**URL:** <https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332>\
**Category:** Using Swift\
**Tags:** bug, concurrency\
**Created:** [October 12, 2024, 10:59pm UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332 "2024-10-12T22:59:24Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![NSFatalError](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/nsfatalerror/32/37725_2.png) [@NSFatalError](https://forums.swift.org/u/NSFatalError)\
**Post date:** [October 12, 2024, 10:59pm UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332/1 "2024-10-12T22:59:24Z")

</div>

Hi! I might have found a possible bug in the compiler, as it seems to allow for a race condition to be created in the Swift 6 language mode, but my understanding could be wrong. I would highly appreciate any insights on the matter.

* * *

With the new Swift 6 language mode (tested with Xcode 16.0 and 16.1 beta 3) I'm able to compile the following code without warnings:

```swift
final class DataSource {
    func fetch() async -> [String] {
        [String(Int.random(in: 0 ..< 100))]
    }
}

final class ViewModel {
    let dataSource = DataSource()
    var data = [String]()

    @MainActor
    func loadInBackground() {
        Task { await load() }
    }

    func load() async {
        data = await dataSource.fetch()
        print(data)
    }
}

// MARK: - Demo
// Just for reproduction purposes, the problem is not SwiftUI-specific

import SwiftUI

struct ContentView: View {
    @State private var viewModel = ViewModel()

    var body: some View {
        Color.blue.onAppear {
            for _ in 0 ..< 10000 {
                viewModel.loadInBackground()
            }
        }
    }
}

```

With multiple rapid calls to `loadInBackground()` multiple tasks are created that access and modify the mutable state, but since the `load()` method is non-isolated and per [SE-0338](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0338-clarify-execution-non-actor-async.md) it switches from the parent task's `MainActor` executor to a generic one, a race condition is created.

If I modify the `loadInBackground` method not to be isolated to any global actor, the compiler produces the following error:

```swift
func loadInBackground() {
    Task { await load() }
    // Xcode 16.1 beta 3 - Error: Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
    // Xcode 16.0 - Error: Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race
    // Side note - it's great to see error messages improve!
}

```

That's indeed correct and I would suspect a similar error should be emitted when the function was marked with a global actor.

Moreover, let's consider the following example, which also compiles without issue in Swift 6:

```swift
Task { @MainActor in
    let viewModel = ViewModel()
    Task {
        for _ in 0 ..< 10000 {
            await viewModel.load()
        }
    }
    for _ in 0 ..< 10000 {
        await viewModel.load()
    }
}

```

While both loops are performed in the same isolation context inherited from the top-level task, the moment we enter the `load()` method the execution is switched to a generic executor, leading to simultaneous state mutation from two call sites.

When the `@MainActor` attribute is removed, both Xcode 16.0 and 16.1 beta 3 give the same error:

```swift
Error: Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race

```

* * *

None of this was possible in Swift 5.10 with strict concurrency checking enabled, as it was before the introduction of [region-based isolation](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md). I'm lead to believe the static isolation checking rules applied to tasks inheriting global actor context from their parents could be too lenient.

Again, I could be wrong about this, as I'm still reading through the evolution proposals and articles from the community in an attempt to understand Swift's concurrency system better. All help is welcome!

---

<div class="post-metadata">

**Author:** ![NSFatalError](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/nsfatalerror/32/37725_2.png) [@NSFatalError](https://forums.swift.org/u/NSFatalError)\
**Post date:** [October 12, 2024, 11:06pm UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332/2 "2024-10-12T23:06:49Z")

</div>

I've also found [an issue on GitHub](https://github.com/swiftlang/swift/issues/76929) that seems to be related.

---

<div class="post-metadata">

**Author:** ![CrystDragon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/crystdragon/32/10461_2.png) [@CrystDragon](https://forums.swift.org/u/CrystDragon)\
**Post date:** [October 13, 2024, 8:10am UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332/3 "2024-10-13T08:10:52Z")

</div>

I agree with you, I also believe there's definitely a safety hole in the current RBI implementation.

---

<div class="post-metadata">

**Author:** ![NSFatalError](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/nsfatalerror/32/37725_2.png) [@NSFatalError](https://forums.swift.org/u/NSFatalError)\
**Post date:** [October 13, 2024, 4:28pm UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332/4 "2024-10-13T16:28:33Z")

</div>

Thanks for confirming @CrystDragon, let’s hope it’s fixed soon.

It’s unfortunate, because if someone already started migrating to Swift 6 and worked around the errors it’s quite possible they work will be partially wasted. As conformance to `Sendable` is a strong requirement and where possible I wanted to avoid it, trusting in and benefiting from RBI, I've already added some code that passed the static checks, but probably won't when the compiler will be fixed.

And this seems to me like the biggest problem - after updating the toolchain "migrated" projects may stop compiling.

---

<div class="post-metadata">

**Author:** ![KeithBauerANZ](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/keithbaueranz/32/27897_2.png) [@KeithBauerANZ](https://forums.swift.org/u/KeithBauerANZ)\
**Post date:** [October 13, 2024, 9:49pm UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332/5 "2024-10-13T21:49:45Z")

</div>

It's pretty similar to [Sendable checking hole when actors call non-mutating async methods on non-Sendable types · Issue #65315 · swiftlang/swift · GitHub](https://github.com/swiftlang/swift/issues/65315), which was marked fixed a long time ago. That bug also references [How can self be accessible in a non-isolated async function of a non-Sendable type? · Issue #71097 · swiftlang/swift · GitHub](https://github.com/swiftlang/swift/issues/71097), also marked closed, but I don't actually believe @hborla 's comment on this one — the `async` on the `mutating` method means there's always an actor hop to call it. And an `async` method on a non-`Sendable` class (as per OP here) is essentially the same problem.

FWIW, Holly has a proposal to change this default behavior of hopping actor at `async`: [[Pitch] Inherit isolation by default for async functions](https://forums.swift.org/t/pitch-inherit-isolation-by-default-for-async-functions/74862) which would actually resolve this issue, but obviously that can't take effect before Swift 7, so this should be fixed now for Swift 6...

---

<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:** [October 14, 2024, 1:28am UTC](https://forums.swift.org/t/calling-non-isolated-async-method-from-a-child-task-inheriting-global-actor-allows-for-a-race-condition-in-swift-6/75332/6 "2024-10-14T01:28:31Z")

</div>

No, this is not the same as that older issue that I closed as correct behavior. In this example, the non-Sendable values are captured in isolated Task closures, which should be considered a region merge under region-based isolation, which should prevent the nonisolated async function from being called. I still believe my answer on that GitHub issue is correct. It is safe to call a nonisolated async method on a non-Sendable type if the caller is already on the generic executor, or if the non-Sendable value is in a disconnected region.
