[翻译]Realm Advanced Guides - Thre

2021-05-16  本文已影响0人  竹本

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 在你遵守下面三条规则时可以实现简单且安全的多线程代码:

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:

当你需要跨线程通信时,根据你的使用场景,你有几个选项:

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:

你可以这样传递线程限制的实例到另一个线程:

  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.
  1. 使用线程限制的对象初始化一个 ThreadSafeReference
  2. 把这个引用传递到其他线程或队列。
  3. 通过调用 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:

在处理冻结对象时,尝试执行以下任一操作都会抛出异常:

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 的两个基本要素是:

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 数据库分两个阶段提交更改:

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 数据库复制树的相关部分进行写入,然后通过更新指针来替换最新版本。

image
  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 (R1, A1, C1), 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.
  1. Realm 被构造为一棵树。Realm 有一个指针指向其最新的版本,V1。
  2. 在写入时,Realm 数据库会基于 V1 创建一个新版本 V2。Realm 数据库复制要修改的对象(R1,A1,C1),而未修改对象的链接继续指向原始版本(B,D)。
  3. 在验证提交之后,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 总结

上一篇下一篇

猜你喜欢

热点阅读