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()
}
}
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.
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.
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?
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.
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.
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.
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.