Navigation

Threading - iOS SDK

To make your iOS and tvOS apps fast and responsive, you must balance the computing time needed to lay out the visuals and handle user interactions with the time needed to process your data and run your business logic. Typically, app developers spread this work across multiple threads: the main or UI thread for all of the user interface-related work, and one or more background threads to compute heavier workloads before sending it to the UI thread for presentation. By offloading heavy work to background threads, the UI thread can remain highly responsive regardless of the size of the workload. But it can be notoriously difficult to write thread-safe, performant, and maintainable multithreaded code that avoids issues like deadlocking and race conditions. MongoDB Realm aims to simplify this for you.

MongoDB Realm enables simple and safe multithreaded code when you follow these three rules:

Don't lock to read:
Realm Database's Multiversion Concurrency Control (MVCC) architecture eliminates the need to lock for read operations. The values you read will never be corrupted or in a partially-modified state. You can freely read from realms on any thread without the need for locks or mutexes. Unnecessary locking would be a performance bottleneck since each thread might need to wait its turn before reading.
Avoid writes on the UI thread if you write on a background thread:
You can write to a realm from any thread, but there can be only one writer at a time. Consequently, write transactions block each other. A write on the UI thread may result in your app appearing unresponsive while it waits for a write on a background thread to complete. If you are using Realm Sync, avoid writing on the UI thread as Sync writes on a background thread.
Don't pass live objects, collections, or realms to other threads:
Live objects, collections, and realm instances are thread-confined: that is, they are only valid on the thread on which they were created. Practically speaking, this means you cannot pass live instances to other threads. However, Realm Database offers several mechanisms for sharing objects across threads.
Tip
See also:

You can have the same realm open on multiple threads as separate realm instances. You are free to read and write with realm instances on the thread where you first opened them. One of the key rules when working with Realm Database in a multithreaded environment is that objects are thread-confined: you may not access the instances of a realm, collection, or object that originated on other threads. Realm Database's Multiversion Concurrency Control (MVCC) architecture means that there could be many active versions of an object at any time. Thread-confinement ensures that all instances in that thread are of the same internal version.

Note
One Version at a Time

A realm instance is designed to work with one version at a time, not several different versions. Consider what Realm Database would have to do to support working with several different versions at once: it would need to store a potentially enormous graph to allow the realm instance to reconcile the different object versions internally. Faced with this, it seems more reasonable to impose the limitation that you cannot pass live instances across threads. This design choice keeps the implementation relatively simple, more space-efficient, and more performant as a result.

When you need to communicate across threads, you have several options depending on your use case:

  • To work with the data on two threads, query for the object on both threads or pass a ThreadSafeReference to the other thread.
  • To react to changes made on any thread, use Realm Database's notifications.
  • To see changes from other threads in the realm on the current thread, refresh your realm instance.
  • To send a fast, read-only view of the object to other threads, "freeze" the object.
  • To keep and share many read-only views of the object in your app, copy the object from the realm.

Instances of Realm, Results, List, and managed Objects are thread-confined. That means you may only use them on the thread where you created them.

You can pass thread-confined instances to another thread as follows:

  1. Initialize a ThreadSafeReference with the thread-confined object.
  2. Pass the reference to the other thread or queue.
  3. Resolve the reference on the other thread's realm by calling Realm.resolve(_:). Use the returned object as normal.
Important

You must resolve a ThreadSafeReference exactly once. Otherwise, the source realm will remain pinned until the reference gets deallocated. For this reason, ThreadSafeReference should be short-lived.

let person = Person(name: "Jane")
let realm = try! Realm()
try! realm.write {
realm.add(person)
}
// Create thread-safe reference to person
let personRef = ThreadSafeReference(to: person)
// Pass the reference to a background thread
DispatchQueue(label: "background").async {
autoreleasepool {
let realm = try! Realm()
try! realm.write {
// Resolve within the transaction to ensure you get the latest changes from other threads
guard let person = realm.resolve(personRef) else {
return // person was deleted
}
person.name = "Jane Doe"
}
}
}

Another way to work with an object on another thread is to query for it again on that thread. But this can get complicated when the object does not have a primary key. You can use ThreadSafeReference on any object, regardless of whether it has a primary key. You can also use it with lists and results.

The downside is that ThreadSafeReference requires some boilerplate. You must remember to wrap everything in an autoreleasepool so the objects do not linger on the background thread. So, it can be helpful to make a convenience extension to handle the boilerplate as follows:

extension Realm {
func writeAsync<T: ThreadConfined>(_ passedObject: T, errorHandler: @escaping ((_ error: Swift.Error) -> Void) = { _ in return }, block: @escaping ((Realm, T?) -> Void)) {
let objectReference = ThreadSafeReference(to: passedObject)
let configuration = self.configuration
DispatchQueue(label: "background").async {
autoreleasepool {
do {
let realm = try Realm(configuration: configuration)
try realm.write {
// Resolve within the transaction to ensure you get the latest changes from other threads
let object = realm.resolve(objectReference)
block(realm, object)
}
} catch {
errorHandler(error)
}
}
}
}
}

This extension adds a writeAsync() method to the Realm class. This method passes an instance to a background thread for you.

Example

Suppose you made an email app and want to delete all read emails in the background. You can now do it with two lines of code. Note that the closure runs on the background thread and receives its own version of both the realm and passed object:

let realm = try! Realm()
let readEmails = realm.objects(Email.self).filter("read == true")
realm.writeAsync(readEmails) { (realm, readEmails) in
guard let readEmails = readEmails else {
// Already deleted
return
}
realm.delete(readEmails)
}

You cannot share realm instances across threads.

To use the same Realm file across threads, open a different realm instance on each thread. As long as you use the same configuration, all Realm instances will map to the same file on disk.

When you open a realm, it reflects the most recent successful write commit and remains on that version until it is refreshed. This means that the realm will not see changes that happened on another thread until the next refresh. A realm on the UI thread -- more precisely, on any event loop thread -- automatically refreshes itself at the beginning of that thread's loop. However, you must manually refresh realm instances that do not exist on loop threads or that have auto-refresh disabled.

Live, thread-confined objects work fine in most cases. However, some apps -- those based on reactive, event stream-based architectures, for example -- need to send immutable copies around to many threads for processing before ultimately ending up on the UI thread. Making a deep copy every time would be expensive, and Realm Database does not allow live instances to be shared across threads. In this case, you can freeze and thaw objects, collections, and realms.

Freezing creates an immutable view of a specific object, collection, or realm. The frozen object, collection, or realm still exists on disk, and does not need to be deeply copied when passed around to other threads. You can freely share the frozen object across threads without concern for thread issues. When you freeze a realm, its child objects also become frozen.

Frozen objects are not live and do not automatically update. They are effectively snapshots of the object state at the time of freezing. Thawing an object returns a live version of the frozen object.

When working with frozen objects, an attempt to do any of the following throws an exception:

  • Opening a write transaction on a frozen realm.
  • Modifying a frozen object.
  • Adding a change listener to a frozen realm, collection, or object.

You can use isFrozen to check if the object is frozen. This is always thread-safe.

Frozen objects remain valid as long as the live realm that spawned them stays open. Therefore, avoid closing the live realm until all threads are done with the frozen objects. You can close frozen realm before the live realm is closed.

Important
On caching frozen objects

Caching too many frozen objects can have a negative impact on the realm file size. "Too many" depends on your specific target device and the size of your Realm objects. If you need to cache a large number of versions, consider copying what you need out of the realm instead.

To modify a frozen object, you must thaw the object. Alternately, you can query for it on an unfrozen realm, then modify it. Calling thaw on a live object, collection, or realm returns itself.

Thawing an object or collection also thaws the realm it references.

// Read from a frozen realm
let frozenTasks = frozenRealm.objects(Task.self)
// The collection that we pull from the frozen realm is also frozen
assert(frozenTasks.isFrozen)
// Get an individual task from the collection
let frozenTask = frozenTasks.first!
// To modify the task, you must first thaw it
// You can also thaw collections and realms
let thawedTask = frozenTask.thaw()
// Check to make sure this task is valid. An object is
// invalidated when it is deleted from its managing realm,
// or when its managing realm has invalidate() called on it.
assert(thawedTask?.isInvalidated == false)
// Thawing the task also thaws the frozen realm it references
assert(thawedTask!.realm!.isFrozen == false)
// Let's make the code easier to follow by naming the thawed realm
let thawedRealm = thawedTask!.realm!
// Now, you can modify the task
try! thawedRealm.write {
thawedTask!.status = "Done"
}

Realm Database provides safe, fast, lock-free, and concurrent access across threads with its Multiversion Concurrency Control (MVCC) architecture.

If you are familiar with a distributed version control system like Git, you may already have an intuitive understanding of MVCC. Two fundamental elements of Git are:

  • Commits, which are atomic writes.
  • Branches, which are different versions of the commit history.

Similarly, Realm Database has atomically-committed writes in the form of transactions. Realm Database also has many different versions of the history at any given time, like branches.

Unlike Git, which actively supports distribution and divergence through forking, a realm only has one true latest version at any given time and always writes to the head of that latest version. Realm Database cannot write to a previous version. This makes sense: your data should converge on one latest version of the truth.

A realm is implemented using a B+ tree data structure. The top-level node represents a version of the realm; child nodes are objects in that version of the realm. The realm has a pointer to its latest version, much like how Git has a pointer to its HEAD commit.

Realm Database uses a copy-on-write technique to ensure isolation and durability. When you make changes, Realm Database copies the relevant part of the tree for writing. Realm Database then commits the changes in two phases:

  • Realm Database writes changes to disk and verifies success.
  • Realm Database then sets its latest version pointer to point to the newly-written version.

This two-step commit process guarantees that even if the write failed partway, the original version is not corrupted in any way because the changes were made to a copy of the relevant part of the tree. Likewise, the realm's root pointer will point to the original version until the new version is guaranteed to be valid.

Example

The following diagram illustrates the commit process:

  1. The realm is structured as a tree. The realm has a pointer to its latest version, V1.
  2. When writing, Realm Database creates a new version V2 based on V1. Realm Database makes copies of objects for modification (A 1, C 1), while links to unmodified objects continue to point to the original versions (B, D).
  3. After validating the commit, Realm Database updates the realm's pointer to the new latest version, V2. Realm Database then discards old nodes no longer connected to the tree.

Realm Database uses zero-copy techniques like memory mapping to handle data. When you read a value from the realm, you are virtually looking at the value on the actual disk, not a copy of it. This is the basis for live objects. This is also why a realm head pointer can be set to point to the new version after the write to disk has been validated.

  • MongoDB Realm enables simple and safe multithreaded code when you follow three rules:

    • don't lock to read
    • avoid writes on the UI thread if you write on background threads or use Realm Sync
    • don't pass live objects to other threads.
  • There is a proper way to share objects across threads for each use case.
  • In order to see changes made on other threads in your realm instance, you must manually refresh realm instances that do not exist on "loop" threads or that have auto-refresh disabled.
  • For apps based on reactive, event-stream-based architectures, you can freeze objects, collections, and realms in order to pass shallow copies around efficiently to different threads for processing.
  • Realm Database's multiversion concurrency control (MVCC) architecture is similar to Git's. Unlike Git, Realm Database has only one true latest version for each realm.
  • Realm Database commits in two stages to guarantee isolation and durability.
Give Feedback

On this page