线程 — Swift SDK
在此页面上
为了确保您的 iOS 和 tvOS 应用速快速运行且响应迅速,您必须在布局视觉效果和处理用户交互所需的计算时间与处理数据和运行业务逻辑所需的时间之间找到平衡。通常,应用开发者将这项工作分散到多个线程中:主线程或用户界面线程处理所有与用户界面相关的工作,一个或多个后台线程负责计算较繁重的工作负载,然后将其发送到用户界面线程进行呈现。通过将繁重的工作移交给后台线程,无论工作负载有多大,用户界面线程都可以保持快速响应。但众所周知,要编写线程安全、高性能且可维护的多线程代码来避免死锁和竞争条件等问题,这可能是一项非常困难的任务。Realm 旨在为您简化这一过程。
本页介绍如何跨线程手动管理域文件和 Realm 对象。 Realm还支持使用 Swift actor 使用 Swift 并发功能管理域访问权限。有关 Realm 的 actor支持的概述,请参阅结合 使用Realm与 Actor - Swift SDK。
要遵循的三项规则
在探索适用于多线程应用的 Realm 工具之前,您需要了解并遵循以下三条规则:
- 无需锁即可读取:
- Realm 的多版本并发控制 (MVCC) 架构消除了为读取操作使用锁的需要。您读取的值永远不会损坏或处于部分修改状态。您可以在任何线程上随意读取同一个 Realm 文件,而不需要锁或互斥锁。不必要的锁定会成为性能瓶颈,因为每个线程在读取之前都可能需要等待。
- 如果在后台线程上进行写入,请避免在用户界面线程上进行同步写入:
- 您可以从任何线程写入 Realm 文件,但一次只能有一个写入者。 因此,同步写事务会相互阻塞。 用户界面线程上的同步写入可能会导致应用在等待后台线程上的写入完成时显示为无响应。 Device Sync在后台线程上进行写入,因此您应避免在具有同步 Realm 的用户界面线程上进行同步写入。
- 不要将活动对象、集合或 Realm 传递给其他线程:
- 活动对象、集合和 Realm 实例具有线程限制:换言之,它们仅在创建它们的线程上有效。 实际上,这意味着您不能将活动实例传递给其他线程。 但是,Realm 提供了几种跨线程共享对象的机制。
执行后台写入
版本 10.26.0 中的新增功能。
使用 writeAsync
,您无需跨线程传递线程安全的引用或冻结对象。 相反,请调用realm.writeAsync
。 您可以为写入完成或失败后在源线程上执行的方法提供一个完成块。
执行后台写入时的注意事项:
异步写入块会关闭 Realm 或使 Realm 无效
您可以显式提交或取消事务
let realm = try! Realm() // Query for a specific person object on the main thread let people = realm.objects(Person.self) let thisPerson = people.where { $0.name == "Dachary" }.first // Perform an async write to add dogs to that person's dog list. // No need to pass a thread-safe reference or frozen object. realm.writeAsync { thisPerson?.dogs.append(objectsIn: [ Dog(value: ["name": "Ben", "age": 13]), Dog(value: ["name": "Lita", "age": 9]), Dog(value: ["name": "Maui", "age": 1]) ]) } onComplete: { _ in // Confirm the three dogs were successfully added to the person's dogs list XCTAssertEqual(thisPerson!.dogs.count, 3) // Query for one of the dogs we added and see that it is present let dogs = realm.objects(Dog.self) let benDogs = dogs.where { $0.name == "Ben" } XCTAssertEqual(benDogs.count, 1) }
等待异步写入完成
SDK 提供了一个Bool
来指示域当前是否正在执行异步写入。 在调用以下任一方法后, isPerformingAsynchronousWriteOperations变量将变为true
:
writeAsync
beginAsyncWrite
commitAsyncWrite
在所有计划的异步写入操作完成之前,该变量将一直保持不变。尽管如此,但这会阻塞关闭域或阻塞域失效。
提交或取消异步写入
要完成异步写入,您或 SDK 必须调用以下任一方法:
当您使用 writeAsync
方法时,SDK 会处理提交或取消事务。 这提供了异步写入的便利,而无需手动保持状态与对象范围相关联。但是,在 writeAsync
块中,您可以显式调用 commitAsyncWrite
或 cancelAsyncWrite
。如果返回时未调用这些方法之一, writeAsync
将执行以下任一操作:
执行写入块中的指令后提交写入操作
返回错误
无论属于哪种情况,这都会完成 writeAsync
操作。
若要更好地控制何时提交或取消异步写事务,请使用 beginAsyncWrite
方法。当使用此方法时,您必须显式提交事务。返回而不提交异步写入会取消事务。beginAsyncWrite
会返回一个 ID,您可以将其传递给 cancelAsyncWrite
。
commitAsyncWrite
异步提交写事务。此步骤会将数据保存到 Realm。commitAsyncWrite
可以接受 onComplete
块。提交完成或因错误而失败后,此块将在源线程上执行。
调用 commitAsyncWrite
会立即返回。这允许调用者在 SDK 在后台线程上执行 I/O 时继续进行操作。此方法会返回一个 ID,您可以将其传递给 cancelAsyncWrite
。这将取消对完成块的待处理调用。这不会取消提交本身。
您可以将对 commitAsyncWrite
的顺序调用进行分组。批处理这些调用可以提高写入性能;特别是当批处理事务较小时。要允许对事务进行分组,请将 isGroupingAllowed
参数设置为 true
。
您可以在 beginAsyncWrite
或 commitAsyncWrite
上调用 cancelAsyncWrite
。当您在 beginAsyncWrite
上调用它时,这会取消整个写事务。当您在 commitAsyncWrite
上调用它时,这只会取消您可能已传递给 commitAsyncWrite
的 onComplete
块。这不会取消提交本身。对于想要取消的 beginAsyncWrite
或 commitAsyncWrite
,您需要有它们的 ID。
跨线程通信
要从不同的线程访问同一个 Realm 文件,您必须在每个需要访问权限的线程上实例化一个 Realm 实例。只要指定相同的配置,所有 Realm 实例都会映射到磁盘上的同一文件。
在多线程环境中使用 Realm 时,关键规则之一是对象具有线程限制:您无法访问源自其他线程的 Realm、集合或对象的实例。 Realm 的多版本并发控制 (MVCC)架构意味着一个对象在任何时候都可能有多个活动版本。 线程限制可确保该线程中的所有实例均为同一内部版本。
当您需要跨线程通信时,根据您的具体用例,您有多种选择:
要在两个线程上修改某个对象,可在这两个线程上查询该对象。
要对任何线程上进行的更改做出响应,请使用 Realm 的通知。
要查看当前线程的域实例中的另一个线程上发生的变更,请刷新您的域实例。
要向其他线程发送对象的快速、只读视图,请" 冻结 "该对象。
要在应用中保留并共享对象的多个只读视图,请从 Realm 复制该对象。
要与其他线程或跨 actor 边界共享 Realm 实例或特定对象,请共享对该 Realm 实例或对象的线程安全引用。 有关详细信息,请参阅传递 ThreadSafeReference。
创建一个串行队列,以便以在后台线程上使用 Realm
在后台线程上使用 Realm 时,请创建一个串行队列。Realm 不支持在并发队列(例如 global()
队列)中使用 Realm 实例。
// Initialize a serial queue, and // perform realm operations on it let serialQueue = DispatchQueue(label: "serial-queue") serialQueue.async { let realm = try! Realm(configuration: .defaultConfiguration, queue: serialQueue) // Do something with Realm on the non-main thread }
跨线程传递实例
Realm
、 Results
、 List
和托管 Objects
的实例是具有线程限制。这意味着您只能在创建实例的线程上使用相应实例。然而,Realm 提供了一种线程安全引用的机制,允许您将在一个线程上创建的实例复制到另一个线程。
符合 Sendable 协议
版本 10.20.0 的新增功能:@ThreadSafe 包装器和 ThreadSafeReference 符合Sendable
如果您使用的是 Swift5.6 或更高版本,则 @ThreadSafe属性包装器 和 ThreadSafeReference 都符合 Sendable。
使用 @ThreadSafe 包装器
10.17.0 版本新增。
您可以将具有线程限制的实例传递给其他线程,如下所示:
使用
@ThreadSafe
属性包装器声明一个引用原始对象的变量。根据定义,@ThreadSafe
-wrapped 变量始终是可选的。将
@ThreadSafe
-wrapped 变量传递给其他线程。像使用任意可选变量一样使用
@ThreadSafe
-wrapped 变量。如果被引用的对象已从 Realm 中删除,则引用变量将变为 nil。
let realm = try! Realm() let person = Person(name: "Jane") try! realm.write { realm.add(person) } // Create thread-safe reference to person var personRef = person // @ThreadSafe vars are always optional. If the referenced object is deleted, // the @ThreadSafe var will be nullified. print("Person's name: \(personRef?.name ?? "unknown")") // Pass the reference to a background thread DispatchQueue(label: "background", autoreleaseFrequency: .workItem).async { let realm = try! Realm() try! realm.write { // Resolve within the transaction to ensure you get the // latest changes from other threads. If the person // object was deleted, personRef will be nil. guard let person = personRef else { return // person was deleted } person.name = "Jane Doe" } }
在另一个线程上处理对象的另一种方法是在该线程上再次查询该对象。但是,如果对象没有主键,要查询它就不是那么容易了。您可以在任何对象上使用 @ThreadSafe
包装器,无论该对象是否具有主键。
例子
下面的示例展示了如何在函数参数中使用 @ThreadSafe
。这对于可能异步运行或在另一个线程上运行的函数非常有用。
提示
如果您的应用会在 async/await
上下文中访问 Realm,请使用 @MainActor
来标记此代码,从而避免出现与线程相关的崩溃。
func someLongCallToGetNewName() async -> String { return "Janet" } func loadNameInBackground( person: Person?) async { let newName = await someLongCallToGetNewName() let realm = try! await Realm() try! realm.write { person?.name = newName } } func createAndUpdatePerson() async { let realm = try! await Realm() let person = Person(name: "Jane") try! realm.write { realm.add(person) } await loadNameInBackground(person: person) } await createAndUpdatePerson()
使用 ThreadSafeReference(旧版 Swift / Objective-C)
在 Realm Swift SDK 版本 10.17.0 之前或 Objective-C 中,您可以按如下方式将具有线程限制的实例传递给其他线程:
使用具有线程限制的对象来初始化 ThreadSafeReference。
将引用传递给其他线程或队列。
通过调用 Realm.resolve(_:) 来解析其他线程的域上的引用。照常使用返回的对象。
重要
必须只解析一次 ThreadSafeReference
。否则,源域将保持固定状态,直到引用被解除分配。因此,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", autoreleaseFrequency: .workItem).async { 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" } }
在另一个线程上处理对象的另一种方法是在该线程上再次查询该对象。但是,如果对象没有主键,要查询它不是那么容易了。您可以在任何对象上使用 ThreadSafeReference
,无论该对象是否具有主键。您还可以将其与列表和结果一起使用。
缺点是 ThreadSafeReference
需要一些样板文件。您必须记住将所有内容都包装在具有适当范围的 autoreleaseFrequency
的 DispatchQueue
中,这样对象就不会在后台线程上停留。因此,通过一个方便的扩展来处理样板文件会很有帮助,如下所示:
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", autoreleaseFrequency: .workItem).async { 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) } } } }
此扩展会在 Realm 类中添加一个 writeAsync()
方法。此方法将为您向后台线程传递一个实例。
例子
假设您创建了一个电子邮件应用,并且想要在后台中删除所有已读电子邮件。您现在可以使用两行代码来实现此目的。请注意,闭包在后台线程上运行,并接收其自己版本的 Realm 和已传递对象:
let realm = try! Realm() let readEmails = realm.objects(Email.self).where { $0.read == true } realm.writeAsync(readEmails) { (realm, readEmails) in guard let readEmails = readEmails else { // Already deleted return } realm.delete(readEmails) }
跨线程使用同一个 Realm
您不能跨线程共享 Realm 实例。
要跨线程使用同一个 Realm 文件,请在每个线程上打开不同的 Realm 实例。 只要使用相同的配置,所有 Realm 实例都将映射到磁盘上的同一文件。
刷新 Realm
当您打开一个 Realm 时,它会反映最近成功的写入提交,并保留在该版本上,直到其进行刷新。这意味着该 Realm 在下次刷新之前不会看到另一个线程上发生的变更。用户界面线程上的 Realm(更准确地说,任何事件循环线程上的 Realm)会在该线程循环开始时自动进行自我刷新。但是,您必须手动刷新循环线程上不存在或禁用自动刷新的 Realm 实例。
if (![realm autorefresh]) { [realm refresh] }
if (!realm.autorefresh) { // Manually refresh realm.refresh() }
冻结对象
在大多数情况下,具有线程限制的活动对象都可以正常运行。但是,一些应用(例如基于事件流的响应式架构的应用)需要将不可变的副本发送到多个线程进行处理,然后才能最终进入界面线程。每次都进行深度复制的开销很高,而且 Realm 不允许跨线程共享活动实例。在这种情况下,您可以冻结和解冻对象、集合及 Realm。
冻结操作会创建特定对象、集合或 Realm 的不可变视图。已冻结的对象、集合或 Realm 仍然存在于磁盘上,而且在传递给其他线程时无需进行深度复制。您可以跨线程随意共享已冻结对象,而不必担心线程问题。冻结某一个 Realm 时,其子对象也会被冻结。
提示
结合使用 ThreadSafeReference 与 Swift Actor
Realm 目前不支持结合使用 thaw()
与 Swift Actor。要跨 actor 边界使用 Realm 数据,请使用 ThreadSafeReference
而不是冻结对象。更多信息,请参阅传递 ThreadSafeReference。
冻结对象是非活动对象,而且不会自动更新。它们实际上是冻结时的对象状态的快照。解冻对象会返回被冻结对象的活动版本。
// Get an immutable copy of the realm that can be passed across threads RLMRealm *frozenRealm = [realm freeze]; RLMResults *dogs = [Dog allObjectsInRealm:realm]; // You can freeze collections RLMResults *frozenDogs = [dogs freeze]; // You can still read from frozen realms RLMResults *frozenDogs2 = [Dog allObjectsInRealm:frozenRealm]; Dog *dog = [dogs firstObject]; // You can freeze objects Dog *frozenDog = [dog freeze]; // To modify frozen objects, you can thaw them // You can thaw collections RLMResults *thawedDogs = [dogs thaw]; // You can thaw objects Dog *thawedDog = [dog thaw]; // You can thaw frozen realms RLMRealm *thawedRealm = [realm thaw];
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 people = realm.objects(Person.self) // You can freeze collections let frozenPeople = people.freeze() assert(frozenPeople.isFrozen) // You can still read from frozen realms let frozenPeople2 = frozenRealm.objects(Person.self) assert(frozenPeople2.isFrozen) let person = people.first! assert(!person.realm!.isFrozen) // You can freeze objects let frozenPerson = person.freeze() assert(frozenPerson.isFrozen) // Frozen objects have a reference to a frozen realm assert(frozenPerson.realm!.isFrozen)
使用冻结对象时,尝试执行以下任一操作都会引发异常:
在冻结的 Realm 中启动写事务。
修改冻结对象。
将变更侦听器添加到冻结的 Realm、集合或对象中。
您可以使用 isFrozen
检查对象是否被冻结。这始终是线程安全的。
if ([realm isFrozen]) { // ... }
if (realm.isFrozen) { // ... }
只要生成冻结对象的活动 Realm 保持打开状态,相应的冻结对象就会保持有效。因此,在所有线程都使用完冻结对象之前,请避免关闭活动 Realm。您可以在活动 Realm 关闭之前先关闭冻结 Realm。
重要
在缓存冻结对象时
缓存太多冻结对象可能会对 Realm 文件大小产生负面影响。“太多”取决于您的特定目标设备和 Realm 对象的大小。如果您需要缓存大量的版本,请考虑将您需要的内容复制到 Realm 以外。
修改冻结对象
要修改冻结对象,您必须解冻该对象。或者,你可以在未冻结的 Realm 中对其进行查询,然后对其进行修改。在活动对象、集合或 Realm 上调用 thaw
会返回自身。
解冻对象或集合也会解冻其引用的 Realm。
// Read from a frozen realm let frozenPeople = frozenRealm.objects(Person.self) // The collection that we pull from the frozen realm is also frozen assert(frozenPeople.isFrozen) // Get an individual person from the collection let frozenPerson = frozenPeople.first! // To modify the person, you must first thaw it // You can also thaw collections and realms let thawedPerson = frozenPerson.thaw() // Check to make sure this person 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(thawedPerson?.isInvalidated == false) // Thawing the person also thaws the frozen realm it references assert(thawedPerson!.realm!.isFrozen == false) // Let's make the code easier to follow by naming the thawed realm let thawedRealm = thawedPerson!.realm! // Now, you can modify the todo try! thawedRealm.write { thawedPerson!.name = "John Michael Kane" }
附加到冻结集合
当附加到冻结集合时,必须同时解冻集合和要附加的对象。在此示例中,我们在冻结 Realm 中查询两个对象:
一个具有 Dog 对象的 List 属性的 Person 对象
一个 Dog 对象
我们必须先解冻这两个对象,然后才能将 Dog 附加到 Person 的 Dog 列表集合中。如果我们只解冻 Person 对象而不解冻 Dog,Realm 会抛出错误。
同样的规则也适用于跨线程传递冻结对象的情形。常见情形可能是在后台线程上调用函数来执行某些工作,而不是阻塞用户界面。
// Get a copy of frozen objects. // Here, we're getting them from a frozen realm, // but you might also be passing them across threads. let frozenTimmy = frozenRealm.objects(Person.self).where { $0.name == "Timmy" }.first! let frozenLassie = frozenRealm.objects(Dog.self).where { $0.name == "Lassie" }.first! // Confirm the objects are frozen. assert(frozenTimmy.isFrozen == true) assert(frozenLassie.isFrozen == true) // Thaw the frozen objects. You must thaw both the object // you want to append and the collection you want to append it to. let thawedTimmy = frozenTimmy.thaw() let thawedLassie = frozenLassie.thaw() let realm = try! Realm() try! realm.write { thawedTimmy?.dogs.append(thawedLassie!) } XCTAssertEqual(thawedTimmy?.dogs.first?.name, "Lassie")
Realm 的线程模型深入剖析
Realm 通过其多版本并发控制 (MVCC) 架构提供安全、快速、无锁和跨线程的并发访问。
与 Git 的比较和对比
如果您熟悉 Git 这样的分布式版本控制系统 ,那么您可能已经对 MVCC 有了直观的理解。Git 的两个基本要素如下:
提交,即原子写入。
分支,即不同版本的提交历史记录。
与此类似,Realm 也采用事务的形式进行原子提交的写入。Realm 在任何时候都有许多不同的历史版本,比如分支。
与通过分叉积极支持分发和发散的 Git 不同,Realm 在任何给定时间只有一个真正的最新版本,并且总是会写入到该最新版本的头部。Realm 无法写入以前的版本。这意味着您的数据收敛于最新的事实版本。
内部结构
域是使用 B+ 树数据结构实现的。顶级节点代表域的一个版本;子节点是该域版本中的对象。该域有一个指向其最新版本的指针,就像 Git 有一个指向其 HEAD 提交的指针一样。
Realm 采用写入时复制技术来确保隔离性和耐久性 。当您进行变更时,Realm 会复制树的相关部分以进行写入。然后,Realm 会分两个阶段提交变更:
Realm 将变更写入磁盘并验证成功。
随后,Realm 将其最新版本指针设置为指向新写入的版本。
这种两步提交过程可保证:即使写入中途失败,原始版本也不会以任何方式损坏。这是因为变更是对树的相关部分的副本进行的。同样,Realm 的根指针将指向原始版本,直到保证新版本有效为止。
例子
下图说明了提交过程:
Realm 采用树的结构。Realm 有一个指向其最新版本 V1 的指针。
在写入时,Realm 会创建一个基于 V1 的新版本 V2。 Realm 创建对象的副本以进行个性(A 1 、 C 1 ),而指向未修改对象的链接将继续指向原始版本(B、D)。
验证提交后,Realm 会将指针更新到最新版本 V2。然后,Realm 会丢弃不再与树连接的旧节点。
Realm 使用内存映射之类的零拷贝技术来处理数据。当您从 Realm 读取值时,您其实是在实际磁盘(而非其副本)上查看该值。这是活动对象的基础。也正是因为如此,系统才能在磁盘写入验证完成后将 Realm 头部指针设置为指向新版本。
总结
遵循以下三个规则时,Realm 可以启用简单且安全的多线程代码:
不用锁进行读取
如果在后台线程上写入或使用Device Sync ,请避免在用户界面线程上进行写入
请勿将活动对象传递给其他线程。
针对每种用例,都有跨线程共享对象的适当方法。
为了查看在您 Realm 实例中其他线程上所进行的变更,您必须手动刷新“循环”线程上不存在的 Realm 实例或禁用自动刷新的 Realm 实例。
对于采用基于事件流的响应式架构的应用,您可以冻结对象、集合和 Realm,以便高效地将浅副本传递到不同的线程进行处理。
Realm 的多版本并发控制 (MVCC) 架构与 Git 类似。与 Git 不同,Realm 对于每个 Realm 实例只有一个真正的最新版本。
Realm 分两个阶段进行提交以保证隔离性和耐久性。
符合 Sendable 协议、不符合 Sendable 协议和具有线程限制的类型
Realm Swift SDK 公共 API 中包含的类型分为以下三大类:
符合 Sendable 协议
不符合 Sendable 协议且不具有线程限制
线程限制
您可以在线程之间共享不符合 Sendable 协议且不受线程限制的类型,但必须对它们进行同步。
除非被冻结,否则具有线程限制的类型仅限于隔离上下文。即使使用同步,也无法在这些上下文之间传递它们。
符合 Sendable 协议 | Non-Sendable | 线程限制 |
---|---|---|
AnyBSON | RLMAppConfiguration | AnyRealmCollection |
AsyncOpen | RLMFindOneAndModifyOptions | AnyRealmValue |
AsyncOpenSubscription | RLMFindOptions | 名单 |
RLMAPIKeyAuth | RLMNetworkTransport | Map |
RLMApp | RLMRequest | MutableSet |
RLMAsyncOpenTask | RLMResponse | 投射 |
RLMChangeStream | RLMSyncConfiguration | RLMArray |
RLMCompensatingWriteInfo | RLMSyncTimeoutOptions | RLMChangeStream |
RLMCredentials | RLMDictionary | |
RLMDecimal128 | RLMDictionaryChange | |
RLMEmailPasswordAuth | RLMEmbeddedObject | |
RLMMaxKey | RLMLinkingObjects | |
RLMMinKey | RLMObject | |
RLMMongoClient | RLMPropertyChange | |
RLMMongoCollection | RLMRealm | |
RLMMongoDatabase | RLMResults | |
RLMObjectId | RLMSection | |
RLMObjectSchema | RLMSectionedResults | |
RLMProgressNotification | RLMSectionedResultsChangeset | |
RLMProgressNotificationToken | RLMSet | |
RLMProperty | RLMSyncSubscription | |
RLMPropertyDescriptor | RLMSyncSubscriptionSet | |
RLMProviderClient | RealmOptional | |
RLMPushClient | RealmProperty | |
RLMSchema | ||
RLMSortDescriptor | ||
RLMSyncErrorActionToken | ||
RLMSyncManager | ||
RLMSyncSession | ||
RLMThreadSafeReference | ||
RLMUpdateResult | ||
RLMUser | ||
RLMUserAPIKey | ||
RLMUserIdentity | ||
RLMUserProfile | ||
ThreadSafe |