数据库与运行时

Salsa 数据库结构体由用户通过 #[salsa::db] 属性来声明。 它包含程序执行所需的全部数据:

#[salsa::db]
struct MyDatabase {
    storage: Storage<Self>,
    maybe_other_fields: u32,
}

这些数据分成两类:

  • 由 Salsa 管理的存储,也就是 Storage<Self> 字段中的内容。这部分是必须存在的。
  • 用户自己定义的其他字段(例如 maybe_other_fields)。它们可以是任意内容,用来让你的数据库接入某些特殊资源或其他上下文。

并行句柄

如果数据库会被多个并行线程共同使用, 那么用户定义的数据库类型必须支持一种 “snapshot” 操作。 这个 snapshot 应当创建出一个可供并行线程使用的数据库克隆。 Storage 本身就支持 snapshotsnapshot 方法会返回 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 负责存储那些“活跃的、进行中的数据”, 例如当前有哪些查询在栈上, 以及当前活跃查询访问了哪些依赖。