[Xcode 14.1] withCheckedContinuation's body will run on background thread in case of starting from main-actor

I found something that looks like a problem in Concurrency's behavior.

withCheckedContinuation's behavior has changed from Xcode 14.1 and earlier than iOS 15.
its body will run on a background thread in case starting from main actor.
Although, iOS 15 and 16 run in the same context(main thread)

Until Xcode 14.0.1, there is no difference between os versions. is this an expected change?

Task { @MainActor () -> Void in
  await withCheckedContinuation { c in    
    // >= iOS 15 : on main-thread
    // < iOS 15 : on background-thread
    c.resume()
  }
}

I created a project reproducing this case.

2 Likes

Same problem happened to me, any one has generic solution?

1 Like

This behavior breaks our current implementations in iOS 14 and earlier.
Find places where using withCheckedContinuation and withUnsafeContinuation, then we need to heal it.

For instance, we can create a small wrapper function that guarantees the closure runs on main-isolated context. like this.

@MainActor
public func withCheckedContinuationOnMain<T>(function: String = #function, _ body: @escaping @MainActor (CheckedContinuation<T, Never>) -> Void) async -> T {
  await withCheckedContinuation { continuation in
    if #available(iOS 15, *) {
      Task { @MainActor in
        body(continuation)
      }
    } else {
      body(continuation)
    }
  }
}

Although I think using Task to dispatch on main-context means, it potentially creates another side-effect.
Task { @MainActor } would make a hop (run event-loop)

So I am waiting for any responses about whether this issue is expected.
Meantime, still can use Xcode 14.0. but it could be a serious problem for developers using ActivityKit API.

Thanks for your report! I'm not sure what's going wrong here, but note that, as a workaround, you should be able to tag the withCheckedContinuation closure as @MainActor directly, without kicking off a new task:

though I agree that it shouldn't be necessary to do so.

Are you by any chance building with the iOS 14 SDK, not just running on iOS 14? You can (and should) build with the latest SDK and set your minimum deployment target back to iOS 14 rather than trying to build with an old SDK.

1 Like

@Joe_Groff @John_McCall
Thanks for replying!

I want you to see this screenshot.

Running on iOS 14.5 SDK.
withCheckedContinuation runs on background even though triggers from Main-actor context.
Main-thread checker raises an runtime error. means happening on the background.

It's not going to happen iOS15+.

withCheckedContinuation does not accept Actor specifying because that closure is not async. There is no chance to hop to the given actor. Xcode warns too.

Also, I don't think the project is using an older SDK.

As I mentioned, this project can reproduce it.
I hope you will try this on.

wrapping with main-actor isolated tasks guarantees runs on the main context after the suspension point.
I mean, I could understand that the operation which is awaiting is not guaranteed to run on which thread.

however, it looks different from the back-deployment version and embedded runtime.
I was just wondering if it's expected or not. Because at least it won't happen until Xcode14.0.1.

It is not expected. We're trying to reproduce it now. To be clear, you're seeing this when running on an iPhone 8 device (not a simulator) with iOS 14.5 installed? And you're using a fresh install of Xcode 14.0.1? Does it also reproduce with Xcode 14.1?

sorry. let me clarify it
It's happening on Xcode 14.1 (14b47b).

It's NOT happening on Xcode 14.0.1 (also 14.0.0) - so still using this version to avoid this issue.

and I'm testing on iOS 14.5 Simulator - not have device for it.

the following lists testing results.

  • Xcode 14.0.1

    • iOS 13+: Runs on main (inherited same context)
  • Xcode 14.1 RC.1

    • iOS 15+: Runs on main (inherited same context)
    • iOS 14: Runs on background (hops to different context)
    • iOS 13: Crashes
  • Xcode 14.1 RC.2

    • iOS 15+: Runs on main (inherited same context)
    • iOS 14: Runs on background (hops to different context)
    • iOS 13: Runs on background (hops to different context) - crashes has fixed.
  • Xcode 14.1 Release

    • iOS 15+: Runs on main (inherited same context)
    • iOS 14: Runs on background (hops to different context)
    • iOS 13: Runs on background (hops to different context)

os versions are all simulator.

1 Like

So this situation is happening on the latest Xcode (14.1)

If it's not happening in your environment. maybe my development environment has something wrong?
Could be solved if I delete all older Xcode versions?

Let me know if you can't reproduce it. I'll show you the instructions.
Thanks for your help. I appreciate it.

I've found a similar problem and trying to search for it brought me to this thread, I think it is related.

In our case we have an actor that stores the continuation from withCheckedContinuation in a dictionary. With Xcode 14.1.0 our code running on the iOS 14 simulator will sometimes crash in the actor. Using the Thead Sanitizer we see it sometimes detects a data race within the actor accessing the mutable storage of the continuations, TSan shows a data race and two different threads accessing the data. Haven't found any problems with iOS 16.1, or when building with Xcode 14.0.1.

In our case, since it is not the MainActor we can't just explicitly move the work to the actor - it should be there already.

I'm trying to boil it down to a small reproduction to file a Feedback, there is too much code involved to simply share it, but wanted to flag this early with the team.

3 Likes

Thanks for the extra details @muukii , I really appreciate it. I was able to reproduce this issue. So far it appears the problem is limited to withCheckedContinuation and withTaskGroup (plus their throwing variants) when running your program in the Simulator (for iOS 14 or older) in Debug mode.

The root cause lies within the backdeployed concurrency runtime dylib packaged in Xcode 14.1 that is being used by the Simulator. When compiling your program in Release mode the problem does not come up because the definition of withCheckedContinuation gets inlined into your program from the SDK, but in Debug mode the Simulator will call into the problematic dylib.

I don't think the inlining behavior of withCheckedContinuation is guaranteed, so as a workaround you can use withUnsafeContinuation, which is guaranteed to be inlined.

Overall, this problem will be fixed in a future Xcode release, but I can't provide a specific version or timeline.

5 Likes

I'm trimmed the code down and have this reproduction:

    func testExample() async {
        actor MyActor {
            var mutableState: [UUID: String] = [:]

            func work(taskCount: Int) async {
                for _ in 0..<taskCount {
                    Task { await perform() }
                }
            }

            private func perform() async {
                await withCheckedContinuation { continuation in
                    mutableState.updateValue("Hello World!", forKey: UUID())
                    continuation.resume()
                }
            }
        }

        let actor = MyActor()
        await actor.work(taskCount: 10)
    }

Can crash on iOS 14 and iOS 15. No withTaskGroup. withUnsafeContinuation does not crash, but still shows some warnings in TSan. Xcode 14.0.1 no issues.

TSan warnings could be spurious. In my testing it only works correctly with Xcode 14 and the 2022 OS versions (iOS 16, etc.).

1 Like

I've submitted FB11755298 if that helps.

1 Like

My feedback ID is FB11707587

Actually the problem seems to be happening on device as well, it isn't limited to the Simulator.
I am getting the crash on Xcode 14.1 (release version) on an iOS 14 physical device.

I confirmed my problem was fixed on Xcode 12.2!
Thank you for working on that.

3 Likes

You mean Xcode 14.2 not 12.2 right?