线程 - .NET SDK
为了使您的 C#/.NET 应用快速运行且响应迅速,您必须在布局视觉效果和处理用户交互所需的计算时间与处理数据和运行业务逻辑所需的时间之间取得平衡。 通常,开发者会将这项工作分散到多个线程中:主线程或用户界面线程用于所有与用户界面相关的工作,一个或多个背景线程用于计算较重的工作负载,然后再将其发送到用户界面线程进行演示。通过将繁重的工作卸载给背景线程,无论工作负载有多大,用户界面线程都可以保持快速响应。但要编写线程安全、高性能且可维护的多线程代码来避免死锁和竞争条件等问题是非常困难的。Realm 旨在为您简化这一过程。
重要
SynchronizationContext 线程
在本页中,我们所说的“主线程”(或“用户界面线程”)和“背景线程”。更准确地说,提及的主线程或用户界面线程是指任何具有 SynchronizationContext ,而没有SynchronizationContext
的线程被视为背景线程。
要遵循的三项规则
在探索适用于多线程应用的 Realm 工具之前,您需要了解并遵循以下三条规则:
- 无需锁即可读取:
- Realm 的多版本并发控制 (MVCC) 架构消除了为读取操作使用锁的需要。您读取的值永远不会损坏或处于部分修改状态。您可以在任何线程上随意读取同一个 Realm 文件,而不需要锁或互斥锁。不必要的锁定会成为性能瓶颈,因为每个线程在读取之前都可能需要等待。
- 避免在用户界面线程上进行同步写入:
- 您可以从任何线程写入Realm 文件,但一次只能有一个写入者。 同步写入事务会相互区块。 因此,主线程上的同步写入可能会导致应用在等待背景线程上的写入完成时显示为无响应。 为防止出现这种情况,SDK 提供了WriteAsync()方法。 有关更多信息,请参阅异步写入。
- 不要将活动对象、集合或 Realm 传递给其他线程:
- 活动对象、集合和 Realm 实例具有线程限制:换言之,它们仅在创建它们的线程上有效。 实际上,这意味着您不能将活动实例传递给其他线程。 但是,Realm 提供了几种跨线程共享对象的机制。
跨线程通信
要从不同的线程访问同一个 Realm 文件,您必须在每个需要访问权限的线程上实例化一个 Realm 实例。只要指定相同的配置,所有 Realm 实例都会映射到磁盘上的同一文件。
在多线程环境中使用 Realm 时,关键规则之一是对象具有线程限制:您无法访问源自其他线程的 Realm、集合或对象的实例。 Realm 的多版本并发控制 (MVCC)架构意味着一个对象在任何时候都可能有多个活动版本。 线程限制可确保该线程中的所有实例均为同一内部版本。
当您需要跨线程通信时,根据您的具体用例,您有多种选择:
刷新 Realm
在主 用户界面 线程(或任何具有事件循环的线程)上,Realm 会在每次事件循环迭代开始时自动刷新对象。在事件循环迭代之间,您将处理快照,因此各个方法始终会看到一致的视图,而不必担心其他线程上发生的情况。
当您最初在线程上打开 Realm 时,其状态将是最近成功的写入提交,并且在刷新之前将保持该版本。 如果线程没有运行循环(后台线程通常属于这种情况),则必须手动调用Realm.Refresh()方法,才能将事务推进到最新状态。
使用Transaction.Commit() 提交写事务时,Realm 也会进行刷新。
注意
未能定期刷新 Realm 可能会导致某些事务版本被“固定”,从而阻止 Realm 重复使用该版本使用的磁盘空间,从而导致文件变大。
异步写入
WriteAsync()方法提供了一种减轻用户界面线程负担的简单方法。 Realm 将异步开始和提交事务,但实际的写入区块将在原始线程上执行。 因此,等待更改是异步的,但回调是在主线程上执行的。 这意味着在写入区块之前创建的对象和查询可以在写入区块内使用,而无需依赖线程安全引用。
以下代码展示了使用AsyncWrite()
创建对象的两个示例。
var testItem = new Item { Name = "Do this thing", Status = ItemStatus.Open.ToString(), Assignee = "Aimee" }; await realm.WriteAsync(() => { realm.Add(testItem); }); // Or var testItem2 = await realm.WriteAsync(() => { return realm.Add<Item>(new Item { Name = "Do this thing, too", Status = ItemStatus.InProgress.ToString(), Assignee = "Satya" }); } );
注意
如果您在后台线程中调用WriteAsync()
,Realm 会在该线程上同步运行,因此相当于调用Write()。
冻结对象
在大多数情况下,具有线程限制的活动对象都可以正常工作。 但是,某些应用程序(例如基于事件流的响应式架构的应用程序)需要将不可变副本发送到许多线程进行处理,然后才能最终进入用户界面线程。 每次都进行深度复制的成本很高,而且 Realm 不允许跨线程共享活动实例。 在这种情况下,您可以冻结对象、collection和 Realm。
冻结操作会创建特定对象、collection或 Realm 的不可变视图,该视图仍存在于磁盘上,并且在传递给其他线程时无需进行深度复制。您可以跨线程随意共享冻结对象,而不必担心线程问题。
使用冻结对象时,尝试执行以下任一操作都会引发异常:
在冻结的 Realm 中启动写事务。
修改冻结对象。
将变更侦听器添加到冻结的 Realm、集合或对象中。
对象一旦冻结,就无法解冻。 您可以使用IsFrozen
方法检查对象是否被冻结。 此方法始终是线程安全的。
要修改冻结对象,请在未冻结的 域 上查询该对象,然后对其进行修改。
冻结对象不是活动对象,也不会自动更新。 它们实际上是冻结时对象状态的快照。
冻结某一个域时,其子对象也会被冻结。
只要生成冻结对象的活动域保持打开状态,相应的冻结对象就会保持有效。因此,在所有线程都使用完冻结对象之前,请避免关闭活动域。您可以在活动域关闭之前先关闭冻结域。
重要
在缓存冻结对象时
缓存太多冻结对象可能会对 Realm 文件大小产生负面影响。“太多”取决于您的特定目标设备和 Realm 对象的大小。如果您需要缓存大量的版本,请考虑将您需要的内容复制到 Realm 以外。
Realm 的线程模型深入剖析
Realm 通过其多版本并发控制 (MVCC) 架构提供安全、快速、无锁和跨线程的并发访问。
与 Git 的比较和对比
如果您熟悉 Git 这样的分布式版本控制系统 ,那么您可能已经对 MVCC 有了直观的理解。Git 的两个基本要素如下:
提交,即原子写入。
分支,即不同版本的提交历史记录。
与此类似,Realm 也采用事务的形式进行原子提交的写入。Realm 在任何时候都有许多不同的历史版本,比如分支。
与通过分叉积极支持分发和发散的 Git 不同,域在任何给定时间只有一个真正的最新版本,并且始终会写入到该最新版本的头部。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 分两个阶段进行提交以保证隔离性和耐久性。