线程 - C++ SDK
要创建高性能应用,开发者必须编写线程安全且可维护的多线程代码,以避免死锁和竞争条件等问题。Realm 提供了专门为高性能多线程应用程序设计的工具。
要遵循的三项规则
在探索适用于多线程应用的 Realm 工具之前,您需要了解并遵循以下三条规则:
- 无需锁即可读取:
- Realm 的多版本并发控制 (MVCC)架构消除了对读取操作使用锁的需要。 您读取的值永远不会损坏或处于部分修改状态。 您可以在任何线程上随意读取同一个Realm 文件,而无需使用锁或互斥锁。 不必要的锁定会成为性能瓶颈,因为每个线程在读取之前都可能需要等待。
- 如果在后台线程上进行写入,请避免在用户界面线程上进行同步写入:
- 您可以从任何线程写入 Realm 文件,但一次只能有一个写入者。 因此,同步写事务会相互阻塞。 用户界面线程上的同步写入可能会导致应用在等待后台线程上的写入完成时显示为无响应。 Device Sync在后台线程上进行写入,因此您应避免在具有同步 Realm 的用户界面线程上进行同步写入。
- 不要将活动对象、集合或 Realm 传递给其他线程:
- 活动对象、集合和 Realm 实例具有线程限制:换言之,它们仅在创建它们的线程上有效。 实际上,这意味着您不能将活动实例传递给其他线程。 但是,Realm 提供了几种跨线程共享对象的机制。
跨线程通信
要从不同的线程访问同一个 Realm 文件,您必须在每个需要访问权限的线程上实例化一个 Realm 实例。只要指定相同的配置,所有 Realm 实例都会映射到磁盘上的同一文件。
在多线程环境中使用 Realm 时,关键规则之一是对象具有线程限制:您无法访问源自其他线程的 Realm、集合或对象的实例。 Realm 的多版本并发控制 (MVCC)架构意味着一个对象在任何时候都可能有多个活动版本。 线程限制可确保该线程中的所有实例均为同一内部版本。
当您需要跨线程通信时,根据您的具体用例,您有多种选择:
要在两个线程上修改某个对象,可在这两个线程上查询该对象。
要对任何线程上进行的更改做出响应,请使用 Realm 的通知。
要查看当前线程的域实例中的另一个线程上发生的变更,请刷新您的域实例。
要与其他线程共享 Realm 实例或特定对象,请共享该 Realm 实例或对象的thread_safe_reference 。
要向其他线程发送对象的快速、只读视图,请" 冻结 "该对象。
跨线程传递实例
realm::realm
、 realm::results
和realm::object
的实例具有线程限制。 这意味着您只能在创建它们的线程上使用它们。
您可以将具有线程限制的实例复制到另一个线程,如下所示:
使用该线程对象初始化一个thread_safe_reference 。
将引用传递给目标线程。
解析目标线程上的引用。 如果引用的对象是 Realm 实例,则通过调用
.resolve()
进行解析;否则,将引用移至realm.resolve()
。 现在,返回的对象在目标线程上受到线程限制,就好像它是在目标线程而不是原始线程上创建的一样。
重要
您必须仅解析一次thread_safe_reference
。 否则,源域将保持固定状态,直到引用被解除分配。 因此, thread_safe_reference
应该是短期的。
// Put a managed object into a thread safe reference auto threadSafeItem = realm::thread_safe_reference<realm::Item>{managedItem}; // Move the thread safe reference to a background thread auto thread = std::thread([threadSafeItem = std::move(threadSafeItem), path]() mutable { // Open the database again on the background thread auto backgroundConfig = realm::db_config(); backgroundConfig.set_path(path); auto backgroundRealm = realm::db(std::move(backgroundConfig)); // Resolve the Item instance via the thread safe // reference auto item = backgroundRealm.resolve(std::move(threadSafeItem)); // ... use item ... }); // Wait for thread to complete thread.join();
在另一个线程上处理对象的另一种方法是在该线程上再次查询该对象。 但如果对象没有主键,查询起来就很困难了。 您可以在任何对象上使用thread_safe_reference
,无论该对象是否具有主键。
跨线程使用同一个 Realm
您不能跨线程共享 Realm 实例。
要跨线程使用同一个 Realm 文件,请在每个线程上打开不同的 Realm 实例。 只要使用相同的配置,所有 Realm 实例都将映射到磁盘上的同一文件。
跨线程传递不可变副本
在大多数情况下,具有线程限制的活动对象都可以正常运行。但是,一些应用(例如基于事件流的响应式架构的应用)需要将不可变的副本发送到多个线程进行处理,然后才能最终进入界面线程。每次都进行深度复制的开销很高,而且 Realm 不允许跨线程共享活动实例。在这种情况下,您可以冻结和解冻对象、集合及 Realm。
冻结操作会创建特定对象、集合或 Realm 的不可变视图。已冻结的对象、集合或 Realm 仍然存在于磁盘上,而且在传递给其他线程时无需进行深度复制。您可以跨线程随意共享已冻结对象,而不必担心线程问题。冻结某一个 Realm 时,其子对象也会被冻结。
冻结对象是非活动对象,而且不会自动更新。它们实际上是冻结时的对象状态的快照。解冻对象会返回被冻结对象的活动版本。
要冻结 域、collection或对象,请调用.freeze()
方法:
auto realm = realm::db(std::move(config)); // Get an immutable copy of the database that can be passed across threads. auto frozenRealm = realm.freeze(); if (frozenRealm.is_frozen()) { // Do something with the frozen database. // You may pass a frozen realm, collection, or objects // across threads. Or you may need to `.thaw()` // to make it mutable again. } // You can freeze collections. auto managedItems = realm.objects<realm::Item>(); auto frozenItems = managedItems.freeze(); CHECK(frozenItems.is_frozen()); // You can read from frozen databases. auto itemsFromFrozenRealm = frozenRealm.objects<realm::Item>(); CHECK(itemsFromFrozenRealm.is_frozen()); // You can freeze objects. auto managedItem = managedItems[0]; auto frozenItem = managedItem.freeze(); CHECK(frozenItem.is_frozen()); // Frozen objects have a reference to a frozen realm. CHECK(frozenItem.get_realm().is_frozen());
使用冻结对象时,尝试执行以下任一操作都会引发异常:
在冻结的 Realm 中启动写事务。
修改冻结对象。
将变更侦听器添加到冻结的 Realm、集合或对象中。
您可以使用 .is_frozen()
检查对象是否被冻结。这始终是线程安全的。
if (frozenRealm.is_frozen()) { // Do something with the frozen database. // You may pass a frozen realm, collection, or objects // across threads. Or you may need to `.thaw()` // to make it mutable again. }
只要生成冻结对象的活动 Realm 保持打开状态,相应的冻结对象就会保持有效。因此,在所有线程都使用完冻结对象之前,请避免关闭活动 Realm。您可以在活动 Realm 关闭之前先关闭冻结 Realm。
重要
在缓存冻结对象时
缓存太多冻结对象可能会对 Realm 文件大小产生负面影响。“太多”取决于您的特定目标设备和 Realm 对象的大小。如果您需要缓存大量的版本,请考虑将您需要的内容复制到 Realm 以外。
修改冻结对象
要修改冻结对象,您必须解冻该对象。或者,你可以在未冻结的 Realm 中对其进行查询,然后对其进行修改。在活动对象、集合或 Realm 上调用 .thaw()
会返回自身。
解冻对象或集合也会解冻其引用的 Realm。
// Read from a frozen database. auto frozenItems = frozenRealm.objects<realm::Item>(); // The collection that we pull from the frozen database is also frozen. CHECK(frozenItems.is_frozen()); // Get an individual item from the collection. auto frozenItem = frozenItems[0]; // To modify the item, you must first thaw it. // You can also thaw collections and realms. auto thawedItem = frozenItem.thaw(); // Check to make sure the item is valid. An object is // invalidated when it is deleted from its managing database, // or when its managing realm has invalidate() called on it. REQUIRE(thawedItem.is_invalidated() == false); // Thawing the item also thaws the frozen database it references. auto thawedRealm = thawedItem.get_realm(); REQUIRE(thawedRealm.is_frozen() == false); // With both the object and its managing database thawed, you // can safely modify the object. thawedRealm.write([&] { thawedItem.name = "Save the world"; });
附加到冻结集合
当追加到冻结的collection时,必须同时解冻包含该collection的对象和要追加的对象。
同样的规则也适用于跨线程传递冻结对象的情形。常见情形可能是在后台线程上调用函数来执行某些工作,而不是阻塞用户界面。
在此示例中,我们在冻结 Realm 中查询两个对象:
Project
具有 对象列表属性的Item
对象一个
Item
对象
必须先解冻这两个对象,然后才能将Item
附加到 上的items
列表集合。 Project
如果我们仅解冻Project
对象而不解冻Item
,则 Realm 会引发错误。
// Get frozen objects. // Here, we're getting them from a frozen database, // but you might also be passing them across threads. auto frozenItems = frozenRealm.objects<realm::Item>(); // The collection that we pull from the frozen database is also frozen. CHECK(frozenItems.is_frozen()); // Get the individual objects we want to work with. auto specificFrozenItems = frozenItems.where( [](auto const& item) { return item.name == "Save the cheerleader"; }); auto frozenProjects = frozenRealm.objects<realm::Project>().where( [](auto const& project) { return project.name == "Heroes: Genesis"; }); ; auto frozenItem = specificFrozenItems[0]; auto frozenProject = frozenProjects[0]; // Thaw the frozen objects. You must thaw both the object // you want to append and the object whose collection // property you want to append to. auto thawedItem = frozenItem.thaw(); auto thawedProject = frozenProject.thaw(); auto managingRealm = thawedProject.get_realm(); managingRealm.write([&] { thawedProject.items.push_back(thawedItem); });
struct Item { std::string name; }; REALM_SCHEMA(Item, name)
struct Project { std::string name; std::vector<realm::Item*> items; }; REALM_SCHEMA(Project, name, items)
调度程序(事件循环)
某些平台或框架会自动设置调度程序(或事件循环),它会在应用程序的生命周期中持续处理事件。Realm C++ SDK 会检测并使用以下平台或框架上的调度器:
macOS、iOS、tvOS、watchOS
Android
Qt
Realm 使用调度程序来安排 Device Sync 上传和下载等工作。
如果您的平台没有受支持的调度程序,或者您想使用自定义调度程序,则可以实现realm::scheduler并将实例传递给用于配置 Realm 的realm::db_config 。 Realm 将使用您传递给它的调度程序。
struct MyScheduler : realm::scheduler { MyScheduler() { // ... Kick off task processor thread(s) and run until the scheduler // goes out of scope ... } ~MyScheduler() override { // ... Call in the processor thread(s) and block until return ... } void invoke(std::function<void()>&& task) override { // ... Add the task to the (lock-free) processor queue ... } [[nodiscard]] bool is_on_thread() const noexcept override { // ... Return true if the caller is on the same thread as a processor // thread ... } bool is_same_as(const realm::scheduler* other) const noexcept override { // ... Compare scheduler instances ... } [[nodiscard]] bool can_invoke() const noexcept override { // ... Return true if the scheduler can accept tasks ... } // ... }; int main() { // Set up a custom scheduler. auto scheduler = std::make_shared<MyScheduler>(); // Pass the scheduler instance to the realm configuration. auto config = realm::db_config{path, scheduler}; // Start the program main loop. auto done = false; while (!done) { // This assumes the scheduler is implemented so that it // continues processing tasks on background threads until // the scheduler goes out of scope. // Handle input here. // ... if (shouldQuitProgram) { done = true; } } }
刷新 Realm
在由调度程序或事件循环控制的任何线程上,Realm 会在每次事件循环迭代开始时自动刷新对象。在事件循环迭代之间,您将处理快照,因此各个方法始终会看到一致的视图,而不必担心其他线程上发生的情况。
当您最初在线程上打开 Realm 时,其状态将是最近成功的写入提交,并且在刷新之前将保持该版本。 如果线程不受运行循环控制,则realm.refresh() 必须手动调用方法才能将事务推进到最新状态。
realm.refresh();
注意
未能定期刷新 Realm 可能会导致某些事务版本被“固定”,从而阻止 Realm 重复使用该版本使用的磁盘空间,并导致文件变大。
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 头部指针设置为指向新版本。