数据库与运行时
Salsa 数据库结构体由用户通过 #[salsa::db] 属性来声明。
它包含程序执行所需的全部数据:
#[salsa::db]
struct MyDatabase {
storage: Storage<Self>,
maybe_other_fields: u32,
}
这些数据分成两类:
- 由 Salsa 管理的存储,也就是
Storage<Self>字段中的内容。这部分是必须存在的。 - 用户自己定义的其他字段(例如
maybe_other_fields)。它们可以是任意内容,用来让你的数据库接入某些特殊资源或其他上下文。
并行句柄
如果数据库会被多个并行线程共同使用,
那么用户定义的数据库类型必须支持一种 “snapshot” 操作。
这个 snapshot 应当创建出一个可供并行线程使用的数据库克隆。
Storage 本身就支持 snapshot。
snapshot 方法会返回 Snapshot<DB> 类型,
从而阻止这些克隆通过 &mut 引用被访问。
Storage 结构体
Salsa 的 Storage 结构体包含了 Salsa 自己需要使用和维护的全部数据。
其中有三个关键组成部分:
Shared结构体,包含所有 snapshot 共享的数据,例如同步信息(如条件变量)。这部分数据会用于取消(cancellation)。- 只有当其他线程处于活跃状态时,
Shared中的数据才会在线程间共享。 某些操作,例如修改 input,需要拿到Shared的&mut句柄。 这个句柄是通过Arc::get_mut获得的; 显然,只有当所有 snapshot 和线程都已停止执行时这才可能发生,因为此时Arc必须只剩唯一一个句柄。
- 只有当其他线程处于活跃状态时,
Routes结构体,包含查找任意 ingredient 所需的信息。这部分数据同样在各个句柄之间共享。 把它从Shared中拆出来,是因为它始终都是真正不可变的; 我们希望在获取Shared的&mut访问权时,仍然能够同时持有Routes的句柄。Runtime结构体,它专属于某一个具体数据库实例。 它保存单个活跃线程的数据,同时也持有一些指向共享数据的链接。
修订计数器的递增
Salsa 的整体模型是:数据库有一个唯一的“主”副本,以及零个或多个 snapshot。
这些 snapshot 并不是直接被拥有的,
而是被包在 Snapshot<DB> 类型中,这个类型只允许 & 解引用。
因此,唯一能通过 &mut 引用访问的数据库,始终只有主数据库。
不过每个 snapshot 都会在 Storage 持有 ingredient 的那个 Arc 上再多持有一个句柄。
每当用户尝试进行某个 &mut 操作,例如修改 input 字段时,
首先就必须取消掉所有并行 snapshot,并等待那些线程结束。
等它们结束之后,我们就可以通过 Arc::get_mut 拿到指向 ingredient 数据的 &mut 引用。
这样一来,我们无需任何 unsafe 代码就能获得 &mut 访问,
并且还能保证其他工作线程已经被成功取消
(否则我们只会把自己送进死锁)。
这里最关键的一点是:
在继续往下执行之前,它会先调用 cancel_other_workers:
#![allow(unused)] fn main() { /// Sets cancellation flag and blocks until all other workers with access /// to this storage have completed. /// /// This could deadlock if there is a single worker with two handles to the /// same database! /// /// Needs to be paired with a call to `reset_cancellation_flag`. fn cancel_others(&mut self) -> &mut Zalsa { debug_assert!( self.zalsa_local .try_with_query_stack(|stack| stack.is_empty()) == Some(true), "attempted to cancel within query computation, this is a deadlock" ); self.handle.zalsa_impl.runtime().set_cancellation_flag(); self.handle .zalsa_impl .event(&|| Event::new(EventKind::DidSetCancellationFlag)); let mut clones = self.handle.coordinate.clones.lock(); while *clones != 1 { clones = self.handle.coordinate.cvar.wait(clones); } // The ref count on the `Arc` should now be 1 let zalsa = Arc::get_mut(&mut self.handle.zalsa_impl).unwrap(); // cancellation is done, so reset the flag zalsa.runtime_mut().reset_cancellation_flag(); zalsa } }
Salsa 运行时
Salsa runtime 为各个 ingredient 提供辅助方法。
例如,它会跟踪活跃查询栈,
并提供在查询之间建立依赖的方法(如 report_tracked_read),
以及解决循环的能力。
它还会跟踪当前修订,以及关于“低持久性”和“高持久性”值上次何时发生变化的信息。
大体上说,ingredient 结构体负责存储那些“静止的数据” 例如缓存值,以及“每个 ingredient 自己持有”的内容。
而 runtime 负责存储那些“活跃的、进行中的数据”, 例如当前有哪些查询在栈上, 以及当前活跃查询访问了哪些依赖。