Strict Concurrency Checking

The app I’m working on runs Realm writes serially using a global actor. To minimize contention, we try to keep the actual write transactions as focused as possible. One technique we’ve been using is to create unmanaged Realm objects off of the write actor and then capture references to those objects in a closure that gets executed on the actor:

let dog = Dog()
dog.name = "Rex"
dog.age = 10

writeRealm { realm in
    realm.add(dog)
}

writeRealm looks like this:

@RealmWriteActor func writeRealm<T>(_ block: @Sendable (Realm) throws -> T) throws -> T {
    try autoreleasepool {
        let realm = try Realm(configuration: ...)
        defer {
            realm.invalidate()
        }
        return try realm.write {
            try block(realm)
        }
    }
}

We’re running into trouble with combining this approach with Strict Concurrency Checking since Dog is not Sendable. We’ve explored a few options, but so far haven’t found anything satisfactory:

  • Use ThreadSafe/ThreadSafeReference - These only work with managed Realm objects. Since we need to capture dog before it’s been added to a Realm, we can’t use this.
  • freeze/thaw - These also only work with objects that have already been added to a Realm.
  • Create an @unchecked Sendable wrapper that holds an unmanaged Realm object. - We were able to get this working similarly to the technique demonstrated in the docs for ThreadSafeReference with the help of API-level property wrappers. However, we weren’t able to come up with an acceptable solution for returning unmanaged Realm objects from the writeRealm closure other than making the wrapper itself the return type and manually unboxing, which added too much complexity at the call site for our taste.

What other techniques would you recommend trying, and what ideas do you have for how Realm could better support Strict Concurrency Checking in the future?

I kind of see what you’re asking but it’s not clear what the specific issue is. e.g. you want to minimize contention so you’re focusing the writes. Is that because you’re writing a large amount of data at once? Or is that because data is being accessed while the write is occurring? Is there a UI issue? Or something else?

The code in the question doesn’t provide an example of why you’re running into trouble - e.g. what is the trouble?

Can you explain the use case a bit so we can evaluate it a bit further and maybe come up with some suggestions?

Hi @Jay, thanks for the reply.

The problem we’re running into is that when we enable Strict Concurrency Checking, we get compile-time warnings when we capture instances of Realm Object subclasses in @Sendable closures or return instances of Realm Object subclasses from actor-isolated methods to non-isolated concurrency contexts.

In the example I shared, the closure passed to writeRealm is @Sendable, so when it captures dog, the compiler emits a warning since Dog is not Sendable.

Similarly, if we were to write this:

let dog = writeRealm { realm in
    return Dog()
}

we would also get a warning since an instance of Dog is returned from actor-isolated writeRealm to a non-isolated context.

We explored avoiding these errors using a wrapper like this:

/// implemented by Dog
protocol UnmanagedCopyable {
    func makeUnmanagedCopy() -> Self
}

final class UnmanagedRealmObject<T>: @unchecked Sendable where T: UnmanagedCopyable {
    init(object: T) {
        // make an unmanaged copy at init so that we'll have our own private
        // copy that no other code can modify
        unmanagedObject = object.makeUnmanagedCopy()
    }

    // disallow direct access to our copy
    private let unmanagedObject: T

    // vend copies rather than the original so that we can guarantee that `unmanagedObject`
    // remains unmanaged
    var object: T {
        unmanagedObject.makeUnmanagedCopy()
    }
}

This resulted in code like this:

let dog = Dog()
dog.name = "Rex"
dog.age = 10

let unmanaged = UnmanagedRealmObject(object: dog)

writeRealm { realm in
    realm.add(unmanaged.object)
}

and

let dog = writeRealm { realm in
    return UnmanagedRealmObject(object: realm.objects(Dog.self).first!)
}.object

While this helps to avoid the warnings, it introduces overhead at runtime (due to the need to make extra copies of the realm objects) and undesirable boilerplate to the code.

The naïve solution to the first scenario would be to instantiate Dog inside of the closure:

writeRealm { realm in
    let dog = Dog()
    dog.name = "Rex"
    dog.age = 10
    realm.add(dog)
}

This might be fine for simple cases, but in some situations, we may need to do expensive computation to obtain the values we need to initialize Dog and/or create many Dogs in a single transaction. In either case, we’d prefer not to block the RealmWriteActor while doing that work so that other writes will not be held up.

Furthermore, this approach doesn’t solve the second scenario where we want to return an unmanaged Realm object from an actor-isolated method to a non-isolated context.

Since the Swift team has said that Strict Concurrency Checking will be on by default in Swift 6, we are thinking ahead about what changes we’ll need to make to our code, including figuring out how to best use Realm in conjunction with these new compile-time checks.

Objects are not meant to be sendable, as objects are not thread/task safe. The way to cross the actor boundary with objects is to first grab a ThreadSafeReference to them and then pass them to your actor.

However, your point about unmanaged objects is accurate. We’ll need to discuss as a team how to better solve this use case, as unmanaged objects should be considered sendable.

1 Like

I am probably misunderstanding the use case here, so feel free to fill in the blanks…

Realm objects are thread confined - once an object is on a thread it “can’t be passed” to other threads and are only valid on the thread they were created. (ignoring sharing objects across threads for the moment)

It almost seems like the Realm objects created within an actor (these are thread isolated objects) are being passed out of the actor to be used elsewhere. Wouldn’t that process go against Realm’s live object functionality?

So using that object within the actor that created it is cool - but passing it in or out would not be.

But - there’s the passing the unmanaged objects - those are not thread confined and really, you can do anything you want with them - they may as well be Swift objects at that point. They are totally independent objects - they just have duplicate property values.

So… why can’t you do that?

My other thought is that unmanaged objects share nothing in common with the object it came from, other than duplicate properties. They have their own discreet memory space so it’s not clear how changing one of them would affect any other object.

However, that also means if you’re implementing primary keys - you’ve got 8 unmanaged objects running around with the same primary key… and that’s trouble as they are really 8 independent objects.

So. now that I said that… I understand the error but perhaps going against or around the core way Realm works is part of the issue?

This is a super interesting question but I am still not clear on what the actual problem is - e.g. If you try to pass an object to another object that doesn’t conform to a protocol that accepts it, the compiler will kick it back. That sounds kinda like what’s going on here. (maybe not)?

1 Like

@Jason_Flax @Jay thanks again for the replies!

Exactly! We have situations where we need a long-lived copy of a value from Realm, so to avoid pinning, we make an unmanaged copy (instead of using ThreadSafe/ThreadSafeReference).

Returning unmanaged copies of Realm objects from an actor-isolated context to a non-isolated context isn’t a problem at runtime. It works just fine! The problem is that at compile-time, the fact that it’s unmanaged isn’t represented by the type system, so Swift can’t tell that it’s actually safe for the unmanaged copy to cross that boundary. Therefore it emits a warning.

@Jason_Flax is there a specific reason that unmanaged objects are not sendable? Or it just hasn’t been implemented yet?

@hershberger I see. So it appears your app is heavily dependent on unmanaged objects. Is there a reason you’re using Realm objects for that task? It seems like using Swift objects in that context would be ideal and then when you need the data persisted, cast to a Realm object to back the Swift objects.

I was thinking it would be a akin to having a JSON managed datastore, and that data is then populated into Swift objects to be passed, massaged etc and then coded back to JSON for storage. Just thinking outside the box here.

1 Like

This would definitely work. The only downside I see is that we’d need to maintain a duplicate class/struct definition.

Unmanaged objects are not Sendable because unmanaged and managed objects are the same type, for the sake of user convenience.

Duplicate class/struct definitions is a decent temporary solution for now.

We had the same issue with the annoying warnings because the compiler sees the unmanaged objects as not Sendable… However, this now became a way bigger problem as starting from iOS 17 the app crashes when we pass an unmanaged object accross actors…

“Precondition failed: Incorrect actor executor assumption; Expected ‘UnownedSerialExecutor(executor: (Opaque Value))’ executor.”