realm-swift
realm-swift copied to clipboard
Opening Realm with async/await in actor causes Crash
How frequently does the bug occur?
All the time
Description
When using the async variant for opening a Realm try await Realm()
inside an actor or and actor marked function (any defined global actors), the app crashes because the just created realm is accessed from the wrong thread afterwards.
Let's look at this function:
func handle() async throws {
let realm = try await Realm() // line 1
print(realm.objects(Dog.self)) // line 2
}
The async opening function has the @MainActor
-attribute so if you define this function inside a normal class following happens:
- We enter the function on an arbitrary queue
- Line 1 is executed:
Realm()
initializer is executed on theMainActor
aka the main thread always. So the openedRealm
is opened on the main thread - We come back to our function on the main thread
- Line 2 is therefore executed on the main thread - the same for which the
Realm
is opened
If the same function is defined inside an actor:
- We enter the function on a certain queue (not main thread)
- Line 1 is executed:
Realm()
initializer is executed on theMainActor
aka the main thread always. So the openedRealm
is opened on the main thread - We come back to our function, but switch back to the previous queue / our actor environment
- Line 2 is therefore not executed on the main thread - but the
Realm
was opened on main thread
Proposed Change
I don't know if you can fix this bug without changing the underlying requirements of how a Realm
object in Swift works. It's definitely not a perfect fit for the new concurrency nature of Swift. Therefore I think a note warning about this issue in the documentation is the minimum. It might need more because predicting this crash requires a developer with good knowledge about the Realm threading model and the new Swift concurrency system.
Sadly Xcode also produces an error if you use the non-async/await variant (try Realm()
) inside an async context, which is most of the time what we want. This is because the initializer is named the same. This could also lead to some confusion or wrong adaption
Stacktrace & log output
2022-04-06 00:48:20.586253+0200 RealmAsyncOpenCrashDemo[14284:228953] *** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff20406d44 __exceptionPreprocess + 242
1 libobjc.A.dylib 0x00007fff201a4a65 objc_exception_throw + 48
2 RealmAsyncOpenCrashDemo 0x0000000104e9f003 -[RLMRealm verifyThread] + 131
3 RealmAsyncOpenCrashDemo 0x0000000104d00efe _ZL18RLMVerifyRealmReadP8RLMRealm + 78
4 RealmAsyncOpenCrashDemo 0x0000000104d02758 RLMGetObjects + 72
5 RealmAsyncOpenCrashDemo 0x0000000104ff82dd $s10RealmSwift0A0V7objectsyAA7ResultsVyxGxmAA0A9FetchableRzlF + 125
6 RealmAsyncOpenCrashDemo 0x0000000104c151fc $s23RealmAsyncOpenCrashDemo15CrashingHandlerC6handleyyYaKFTY2_ + 156
7 RealmAsyncOpenCrashDemo 0x0000000104c13fc1 $s23RealmAsyncOpenCrashDemo11ContentViewV4bodyQrvg7SwiftUI05TupleG0VyAE6ButtonVyAE4TextVG_ALtGyXEfU_yycfU0_yyYaYbKcfU_TQ1_ + 1
8 libswift_Concurrency.dylib 0x00007fff6fa509e1 _ZL22completeTaskAndReleasePN5swift12AsyncContextEPNS_10SwiftErrorE + 1
)
libc++abi: terminating with uncaught exception of type NSException
Can you reproduce the bug?
Yes, always
Reproduction Steps
Use this code:
class CrashfreeHandler {
func handle() async throws {
let realm = try await Realm()
print(realm.objects(Dog.self))
}
}
actor CrashingHandler {
func handle() async throws {
let realm = try await Realm()
print(realm.objects(Dog.self))
}
}
or just use the sample project with these code snippets on an iOS project: https://github.com/alexeichhorn/RealmAsyncOpenCrashDemo
Version
10.25.0
What SDK flavour are you using?
Local Database only
Are you using encryption?
No, not using encryption
Platform OS and version(s)
iOS 15.4 (Simulator and real device)
Build environment
Xcode version: 13.3 Dependency manager and version: SPM
Hi @alexeichhorn Thank you for the info. We will look in to that
We had the same issue on our project as well using Realm 10.26.0.
I ended up with a workaround by calling let realm = try Realm(queue: nil)
which is the same as calling the try Realm()
in a non-async version in an async
function to make sure the returned realm instance is not on main thread.
I ended up mapping to lightweight structs when returning any data from a function using Realm. This can have a slight overhead, but ensures items are passed as a copy instead of references.
Here's an example of an actor
that that performs backgrounded "utility" tasks using anasync
function. Also mixed in a GCD for example. Easy way to remember: you cannot use a previously instantiated realm instance anytime after an await
is used in the same scope, which actually makes sense.
await Realm()
seems nonsensical to me, basically asking to yield the current run loop, asking the system to coordinate concurrency for some work, i picture something like this let realm = Thread.random { returning(Realm()) }
actor BackgroundTasks {
static let GCDWorker = DispatchQueue(label: "experiments", qos: .utility, autoreleaseFrequency: .workItem)
func example() async {
let olderThan = 2.months.ago!
// await for filtered frozen objects for reading
let filteredChallenges: Results<Challenge> = await withCheckedContinuation { continuation in
// We are confined to one thread in this block, go to town
let realm = try! Realm()
let challenges = realm.objects(Challenge.self).where { $0.refreshed_at < olderThan }
continuation.resume(returning: challenges.freeze())
}
log.info("We filtered \(filteredChallenges.count) challenges, with ids \(filteredChallenges.map {$0.id} )")
// await for old challenges to be deleted, and return the count that were deleted
let numDeleted: Int = await withCheckedThrowingContinuation { continuation in
let realm = try! Realm()
let challenges = realm.objects(Challenge.self).where { $0.refreshed_at < olderThan }
realm.write {
realm.delete(challenges)
}
continuation.resume(returning: challenges.count)
}
log.info("We just deleted \(numDeleted) challenges")
// Returns immediately
GCDWorker.async {
let realm = try! Realm(configuration: .defaultConfiguration, queue: GCDWorker)
let groups = realm.objects(Group.self).where { $0.refreshed_at < olderThan }
try! realm.write {
realm.delete(groups)
}
log.info("deleted \(groups.count) groups")
}
log.info("GCD is doing stuff, i cant tell you anything ")
}
}
do {
let actor = BackgroundTasks()
Task.detached(priority: .background) {
await actor.example()
}
}
Any news on this topic?
Currently I have the problem that I have app code which runs perfectly fine with Xcode 13.4.1 and Swift 5.6.1.
But with Xcode 14 Beta 6 Swift 5.7 it is instantly crashing with
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
Any news on this topic?
Currently I have the problem that I have app code which runs perfectly fine with Xcode 13.4.1 and Swift 5.6.1. But with Xcode 14 Beta 6 Swift 5.7 it is instantly crashing with
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'
Same issue here. I'm forced to use 5.7 to avoid thread races which crash the app otherwise, but it keeps giving me the incorrect realm error immediately on 5.7. Please give us any update / workaround.
Using Realm in a non-@MainActor
async function is currently not supported. In Swift 5.6 it would often work by coincidence because execution after an await
would continue on whatever thread the awaited thing ran on, so await Realm()
in an async function would result in the code following that running on the main thread until your next call to an actor-isolated function. Swift 5.7 instead hops threads whenever changing actor isolation contexts, so an unisolated async function always run on a background thread instead.
If you have code which uses await Realm()
and works in 5.6, marking the function as @MainActor
will make it work with Swift 5.7 and function very similarly to what it happened to be doing in 5.6.
@LilaQ @Drag0ndust Have you tried the workaround proposed by @yliu342: let realm = try Realm(queue: nil)
.
It's more a compiler issue than one of Realm probably, because the compiler produces an error if there is an equivalent async variant. However, in this case the async variant should not be used except you are using a synced Realm.
@tgoyne Thanks, that did the trick. Now I'm finally rid of the thread race crashes.
...However, in this case the async variant should not be used except you are using a synced Realm.
The fix doesn't work for me. I use RealmSync and need to open the Container with Realm.asyncOpen(...)
What would be the suggestion to do the asyncOpen of the remote realm?
Any news on this topic?
Any news on this topic?
Also looking for an assist on the issue.
Mainly, I think it would be helpful if the naming were updated to reflect that when we are using async / await patterns, invoking a function that says "asyncOpen" sort if feels like it should be usable with async / await for opening a realm.
I am also using a realm that is synced with the new MongoDB Realm Sync service.
Any news???
We will be working on this in the next quarter.
So you will work on this issue earliest next year?
gently asking for an update since last ETA has passed, thanks
This is now included in release 10.38.3:
@MainActor function mainThreadFunction() async throws {
// These are identical: the async init continues to produce a
// MainActor-confined Realm if no actor is supplied
let realm1 = try await Realm()
let realm2 = try await Realm(MainActor.shared)
}
// A simple example of a custom global actor
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()
}
@BackgroundActor backgroundThreadFunction() async throws {
// Explicitly specifying the actor is required for everything but MainActor
let realm = try await Realm(actor: BackgroundActor.shared)
try await realm.write {
_ = realm.create(MyObject.self)
}
// Thread-confined Realms would sometimes throw an exception here, as we
// may end up on a different thread after an `await`
print("\(realm.objects(MyObject.self).count)")
}
actor MyActor {
// An implicitly-unwrapped optional is used here to let us pass `self` to
// `Realm(actor:)` within `init`
var realm: Realm!
init() async throws {
realm = try await Realm(actor: self)
}
var count: Int {
realm.objects(MyObject.self).count
}
func create() async throws {
try await realm.asyncWrite {
realm.create(MyObject.self)
}
}
}
// This function isn't isolated to the actor, so each operation has to be async
func createObjects() async throws {
let actor = try await MyActor()
for _ in 0..<5 {
await actor.create()
}
print("\(await actor.count)")
}
// In an isolated function, an actor-isolated Realm can be used synchronously
func createObjects(in actor: isolated MyActor) async throws {
await actor.realm.write {
actor.realm.create(MyObject.self)
}
print("\(actor.realm.objects(MyObject.self).count)")
}