realm-swift: 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 theMainActoraka the main thread always. So the openedRealmis 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
Realmis 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 theMainActoraka the main thread always. So the openedRealmis 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
Realmwas 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
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 4
- Comments: 17 (4 by maintainers)
Using Realm in a non-
@MainActorasync function is currently not supported. In Swift 5.6 it would often work by coincidence because execution after anawaitwould continue on whatever thread the awaited thing ran on, soawait 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@MainActorwill make it work with Swift 5.7 and function very similarly to what it happened to be doing in 5.6.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.
We will be working on this in the next quarter.
Here’s an example of an
actorthat that performs backgrounded “utility” tasks using anasyncfunction. Also mixed in a GCD for example. Easy way to remember: you cannot use a previously instantiated realm instance anytime after anawaitis 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 thislet realm = Thread.random { returning(Realm()) }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 thetry Realm()in a non-async version in anasyncfunction to make sure the returned realm instance is not on main thread.This is now included in release 10.38.3:
Any news on this topic?
@tgoyne Thanks, that did the trick. Now I’m finally rid of the thread race crashes.
@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.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.'