Realm Advanced Guides - Threading
Realm 高级教程 —— 多线程
原文地址:https://docs.mongodb.com/realm/sdk/ios/advanced-guides/threading/
Overview 概述
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.
为了让你的 iOS 和 tvOS 应用快速响应,你必须平衡用于布局视觉效果和处理用户交互的时间与处理数据和运行业务逻辑的时间。通常,应用开发者把这些工作分布在多个线程上:主线程或 UI 线程用于处理所有用户交互相关的工作,以及一个或多个后台线程用于计算更繁重的工作负载,然后发送给 UI 线程展示。通过将繁重的工作转移到后台线程,UI 线程仍然可以保持高度响应,而不管工作负载的大小。但是,众所周知,要编写线程安全、高性能、可维护的多线程代码,并避免死锁和竞争条件等问题是非常困难的。MongoDB Realm 旨在为你简化这一点。
Three Rules to Keep in Mind 时刻记住三条规则
MongoDB Realm enables simple and safe multithreaded code when you follow these three rules:
MongoDB Realm 在你遵守下面三条规则时可以实现简单且安全的多线程代码:
-
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.
-
读的时候不要加锁:
Realm 数据库的多版本并发控制(MVCC)架构消除了给读操作加锁的需求。你读取的值永远不会损坏或处于部分修改状态。你可以在任意线程自由从 realm 中读取数据,而不需要锁或做互斥。不必要的锁可能成为性能瓶颈,因为每个线程在读之前可能都要等待轮到它。
-
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.
-
如果你在后台线程写入,就避免也在 UI 线程写入:
你可以在任意线程写入 realm,但同一时间只能有一个写入者。因此,写事务会互相阻塞。在 UI 线程的写操作可能导致你的应用看起来没有响应,而它要等一个后台线程的写操作完成。如果你使用同步 Realm,请避免像在后台线程同步写入一样在 UI 线程做写操作。
-
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.
-
不要将活动对象、集合或 realm 传递给其他线程:
活动对象、集合和 realm 实例是线程限制的:也就是说,它们仅在创建它们的线程上有效。实际上,这意味着您不能将活动的实例传递给其他线程。不过,Realm 数据库提供了几种跨线程共享对象的机制。
Communication Across Threads 跨线程通信
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.
你可以在多个线程上作为单独的 realm 实例打开同一个 realm。你可以在你第一次打开它们的线程上使用随意使用 realm 实例来进行读写。在多线程环境中处理 Realm 数据库时的一条关键规则是对象是线程限制的:你可能无法访问源于其他线程的 realm 实例、集合或对象。Realm 数据库多版本并发控制(MVCC)架构意味着一个对象可能随时有多个活动版本。线程限制确保了该线程中的所有实例都是相同的内部版本。
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.
一次一个版本
Realm 实例被设计为一次只使用一个版本,而不是多个不同的版本。考虑一下 Realm 数据库如何才能支持一次使用多个不同的版本:它需要存储一个潜在的巨大的图,以允许 realm 实例在内部协调不同的对象版本。面对这种情况,设置成不能跨线程传递活动实例的限制似乎更为合理。这种设计选择使实现相对简单,更节省空间,因此性能更高。
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.
- 要处理两个线程上的数据,可以在两个线程上都查询相应对象,或者传递一个
ThreadSafeReference
对象到另一个线程。 - 要对任意线程上的改动做出反应,可以使用 Realm 数据库的通知。
- 要在当前线程查看 realm 中来自其他线程的改动,可以刷新你的 realm 实例。
- 要向其他线程发送一个对象的快速的只读视图,可以“冻结”这个对象。
- 要在你的应用中保留和共享一个对象的多个只读视图,可以从 realm 中复制该对象。
Pass Instances Across Threads 跨线程传递实例
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.
Realm、结果、列表和托管对象的实例都是线程限制的。这意味着你可能只能在你创建它们的线程中使用它们。
You can pass thread-confined instances to another thread as follows:
你可以这样传递线程限制的实例到另一个线程:
- Initialize a
ThreadSafeReference
with the thread-confined object. - Pass the reference to the other thread or queue.
- Resolve the reference on the other thread’s realm by calling
Realm.resolve(_:)
. Use the returned object as normal.
- 使用线程限制的对象初始化一个
ThreadSafeReference
。 - 把这个引用传递到其他线程或队列。
- 通过调用
Realm.resolve(_:)
解析另一个线程的 realm 的引用。正常使用返回的对象。
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.重要
你必须准确的只解析
ThreadSafeReference
一次。否则,源 realm 将保持固定,直到该引用被释放。因此,ThreadSafeReference
应该是短期的。
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.
在另一个线程上使用对象的另一个方法是在那个线程上再查询一次。但是当对象没有主键时,这会变得很复杂。你可以对任意对象使用 ThreadSafeReference
,不论它是否有主键。你也可以将其用于列表和结果。
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:
ThreadSafeReference
的缺点是它需要一些模板文件。您必须记住将所有内容包装在自动释放池中,这样对象就不会停留在后台线程中。因此,对模板文件进行如下方便的扩展是很有帮助的:
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.
这个扩展添加了一个 writeAsync()
方法到 Realm 类中。该方法帮你将实例传递给后台线程。
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:
例子
假设你制作了一个电子邮件应用程序,并希望在后台删除所有已阅读的电子邮件。现在你可以用两行代码来完成。请注意,闭包在后台线程上运行,并接收到自己版本的 realm 和传递对象:
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)
}
Use the Same Realm Across Threads 跨线程使用同一个 Realm
You cannot share realm instances across threads.
你不能跨线程共享 realm 实例。
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.
想要跨线程使用同一个 Realm 文件,可以在每个线程打开一个不同的 realm 实例。只要你使用相同的配置,所有的 Realm 实例都会映射到磁盘上的同一个文件。
Refreshing Realms 刷新 Realm
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.
当你打开一个 realm,它会反映最近一次成功的写提交,并保持在该版本上直到被刷新。这意味着在下次刷新之前,realm 将不会看到另一个线程上发生的更改。UI线程——更准确地说,任何事件循环线程——上的 realm 在该线程的循环开始时自动刷新自身。但是,必须手动刷新不在循环线程上或已禁用自动刷新的 realm 实例。
if (!realm.autorefresh) {
// Manually refresh
realm.refresh()
}
Frozen Objects 冻结对象
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.
活动、线程限制的对象在大多数情况下都工作良好。但是,某些应用——例如那些基于响应式的、基于事件流架构的——需要将不可变的副本发送到多个线程进行处理,然后才能最终发到 UI 线程上。每次制作一个深度拷贝的成本很高,而且 Realm 数据库不允许跨线程共享活动实例。在这种情况下,可以冻结和解冻对象、集合和 realm。
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.
冻结会创建特定对象、集合或 realm 的不可变视图。冻结的对象、集合或 realm 仍然在存在于磁盘上,并且在传给其他线程时不需要深度拷贝。你可以自由的跨线程共享冻结的对象,而不用担心线程问题。当你冻结一个 realm 时,它的子对象也会冻结。
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.
冻结的对象不是活动,不会自动更新。它们实际上是冻结时对象状态的快照。解冻一个对象会返回冻结对象的实时版本。
let realm = try! Realm()
// Get an immutable copy of the realm that can be passed across threads
let frozenRealm = realm.freeze()
assert(frozenRealm.isFrozen)
let tasks = realm.objects(Task.self)
// You can freeze collections
let frozenTasks = tasks.freeze()
assert(frozenTasks.isFrozen)
// You can still read from frozen realms
let frozenTasks2 = frozenRealm.objects(Task.self)
assert(frozenTasks2.isFrozen)
let task = tasks.first!
assert(!task.realm!.isFrozen)
// You can freeze objects
let frozenTask = task.freeze()
assert(frozenTask.isFrozen)
// Frozen objects have a reference to a frozen realm
assert(frozenTask.realm!.isFrozen)
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.
-
在冻结的 realm 上打开写事务。
-
修改冻结的对象。
-
向冻结的 realm、集合或对象添加修改监听器。
You can use isFrozen
to check if the object is frozen. This is always thread-safe.
你可以使用 isFrozen
检查对象是否被冻结。这个属性总是线程安全的。
if (realm.isFrozen) {
// ...
}
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.
只要生成冻结对象的活动 realm 保持打开状态,冻结对象就持续有效。因此,在所有线程都处理完冻结的对象之前,避免关闭活动 realm。你可以在关闭活动 realm 之前关闭冻结 realm。
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.
重要
关于缓存冻结对象
缓存过多的冻结对象会对 realm 文件大小产生负面影响。“太多”取决于您的特定目标设备和 realm 对象的大小。如果需要缓存大量版本,请考虑将所需内容复制到 realm 之外。
Modify a Frozen Object 修改冻结对象
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.
要修改一个冻结对象,你必须解冻这个对象。或者,你可以在一个未冻结的 realm 上查询它,然后再修改。对活动的对象、集合或 realm 调用解冻方法将返回它们自身。
Thawing an object or collection also thaws the realm it references.
解冻对象或集合也会解冻它引用的 realm。
// 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’s Threading Model in Depth
Realm Database provides safe, fast, lock-free, and concurrent access across threads with its Multiversion Concurrency Control (MVCC) architecture.
Realm 数据库通过其多版本并发控制(MVCC)架构提供安全、快速、无锁且并发的跨线程访问。
Compared and Contrasted with Git 与 Git 的比较和对比
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:
如果你熟悉 Git 这样的分布式版本控制系统,您可能已经对 MVCC 有了直观的了解。Git 的两个基本要素是:
- 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.
类似地,Realm 数据库以事务的形式进行原子提交的写操作。Realm 数据库在任意给定的时间也有许多不同版本的历史记录,就像分支。
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.
与 Git 不同,Git 通过分叉来积极支持分布和差异,realm 在任何给定的时间只有一个真正的最新版本,并且总是向最新版本的头部写入。Realm 数据库无法写入到以前的版本。这是有道理的:你的数据应该集中在一个真实的最新版本中。
Internal Structure 内部结构
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 是使用 B+ 树的数据结构实现的。顶级节点表示 realm 的一个版本;子节点是该 realm 版本中的对象。Realm 有一个指向其最新版本的指针,就像 Git 有一个指向其 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 数据库使用写时拷贝技术来确保隔离性和持久性。进行更改时,Realm 数据库会复制树的相关部分以供写入。然后 Realm 数据库分两个阶段提交更改:
- Realm Database writes changes to disk and verifies success.
- Realm Database then sets its latest version pointer to point to the newly-written version.
- Realm 数据库把改动写入磁盘并验证成功。
- 然后,Realm 数据库将最新版本指针指向新近写入的版本。
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.
这种两步提交过程保证了即使中途写入失败,原始版本也不会以任何方式损坏,因为更改是对树的相关部分的副本所做的。同样,realm 的根指针将一直指向原始版本,直到新版本被保证是有效的。
EXAMPLE
例子
The following diagram illustrates the commit process:
下图说明了提交过程:
Realm Database copies the relevant part of the tree for writes, then replaces the latest version by updating a pointer.
Realm 数据库复制树的相关部分进行写入,然后通过更新指针来替换最新版本。
- The realm is structured as a tree. The realm has a pointer to its latest version, V1.
- When writing, Realm Database creates a new version V2 based on V1. Realm Database makes copies of objects for modification (R1, A1, C1), while links to unmodified objects continue to point to the original versions (B, D).
- 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 被构造为一棵树。Realm 有一个指针指向其最新的版本,V1。
- 在写入时,Realm 数据库会基于 V1 创建一个新版本 V2。Realm 数据库复制要修改的对象(R1,A1,C1),而未修改对象的链接继续指向原始版本(B,D)。
- 在验证提交之后,Realm 数据库将 realm 的指针更新到新的最新版本,V2。然后,Realm 数据库会丢弃不再连接到树的旧节点。
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.
Realm 数据库使用零拷贝技术(如内存映射)来处理数据。当你从 realm 读取值时,实际上是在查看实际磁盘上的值,而不是它的副本。这是活动对象的基础。这也是为什么可以在磁盘的写入被验证后,可以直接将 realm 头指针设置为指向新版本。
Summary 总结
-
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.
-
MongoDB Realm 在你遵守下面三条规则时可以实现简单且安全的多线程代码:
- 读的时候不要加锁
- 如果你在后台线程写入或者使用同步 Realm,就避免也在 UI 线程写入
- 不要将活动对象传递到其他线程
-
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.
-
为了查看其他线程在 realm 实例中上所做的更改,必须手动刷新不存在于“循环”线程上的或已禁用自动刷新的 realm 实例。
-
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 数据库的多版本并发控制(MVCC)架构类似于Git。与Git不同之处在于,Realm 数据库对于每个 realm 只有一个真正的最新版本。
-
Realm Database commits in two stages to guarantee isolation and durability.
-
Realm 数据库分两个阶段提交以保证隔离性和持久性。