数据库和运行时

Salsa 数据库结构体通过 #[salsa::db] 声明。它包含程序执行所需的所有数据:

#[salsa::db(jar0...jarn)]
struct MyDatabase {
    storage: Storage<Self>,
    maybe_other_fields: u32,
}

该数据分为两类:

  • Salsa 管理和存储的数据,包含在 Storage<Self> 中。此数据为必须的。
  • 使用者定义的其他字段(如 maybe_other_fields)。这可以是任何东西,让你访问特殊资源或其他任何资源。

并行句柄

当跨并行线程使用时,使用者定义的数据库类型必须支持“快照” (snapshot) 操作。

此快照应创建可供并行线程使用的数据库的克隆。

Storage 操作本身支持 snapshotSnaphot方法返回 Snapshot<DB> 类型,防止通过 &mut 访问这些克隆。

Storage

Salsa 的 Storage 结构体包含 Salsa 本身使用和处理的所有数据。有三个关键:

  • Shared 结构体:包含跨所有快照存储的数据。数据主要是 Jars 和配料 一章中描述的配料,但也包含一些同步信息 (cond var)。这是用于取消 (cancellation) 的,如下所述。
    • Shared 中的数据只有在其他线程处于活动状态时才会跨线程共享。
    • 一些操作,如改变输入,需要 Shared&mut 句柄。这是通过 Arc::get_mut 方法获得的;显然,只有当所有快照和线程都停止执行时,这才是可能的,因为必须只有一个指向 arc的句柄。
  • Routes 结构体包含查找任何特定配料的信息,这也在所有句柄之间共享,其构造也在 Jars 和配料 一章中描述了。RoutesShared 中分离出来,是因为它在任何时候都是真正不可变的,并且我们希望能够在获得对 Shared&mut 访问权限的同时持有 Routes 的句柄。
  • Runtime 结构体特定于具体的数据库实例。它包含单个活动线程的数据,以及一些指向其自身的共享数据。

修订计数递增和获取 Jars 的可变性

Salsa 的一般模型是,数据库只有一个“主”副本,可能还有多个快照。快照不是直接拥有的,而是装在只允许 &-deref 的 Snapshot<DB> 类型中,因此唯一可以使用 &mut-ref 访问的数据库是主数据库。然而,每个快照只在存储配料的 Storage 中的 Arc 上有另一个句柄。

无论何时使用者尝试执行 &mut 操作,例如修改输入字段,都需要首先取消所有并行快照,并等待这些并行线程完成。快照完成后,我们可以使用 Arc::get_mut 获取对配料数据的 &mut。这允许我们在没有任何不安全代码的情况下获得 &mut 访问权限,并保证已经成功地取消了其他工作线程(否则我们将陷入死锁)。

通过 jars_mut 方法获取数据库的 &mut 访问权限:

    /// Gets mutable access to the jars. This will trigger a new revision
    /// and it will also cancel any ongoing work in the current revision.
    /// Any actual writes that occur to data in a jar should use
    /// [`Runtime::report_tracked_write`].
    pub fn jars_mut(&mut self) -> (&mut DB::Jars, &mut Runtime) {
        // Wait for all snapshots to be dropped.
        self.cancel_other_workers();

        // Increment revision counter.
        self.runtime.new_revision();

        // Acquire `&mut` access to `self.shared` -- this is only possible because
        // the snapshots have all been dropped, so we hold the only handle to the `Arc`.
        let shared = Arc::get_mut(&mut self.shared).unwrap();

        // Inform other ingredients that a new revision has begun.
        // This gives them a chance to free resources that were being held until the next revision.
        let routes = self.routes.clone();
        for route in routes.reset_routes() {
            route(&mut shared.jars).reset_for_new_revision();
        }

        // Return mut ref to jars + runtime.
        (&mut shared.jars, &mut self.runtime)
    }

关键的一点是它在往下之前调用 cancel_other_workers

    /// 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!
    fn cancel_other_workers(&mut self) {
        loop {
            self.runtime.set_cancellation_flag();

            // If we have unique access to the jars, we are done.
            if Arc::get_mut(&mut self.shared).is_some() {
                return;
            }

            // Otherwise, wait until some other storage entities have dropped.
            // We create a mutex here because the cvar api requires it, but we
            // don't really need one as the data being protected is actually
            // the jars above.
            //
            // The cvar `self.shared.cvar` is notified by the `Drop` impl.
            let mutex = parking_lot::Mutex::new(());
            let mut guard = mutex.lock();
            self.shared.cvar.wait(&mut guard);
        }
    }

Salsa 运行时

Salsa 运行时提供了被配料访问的帮助方法。例如,它跟踪活动的查询堆栈,并包含用于在查询之间添加依赖关系的方法(如 report_track_read)或处理循环。它还跟踪当前修订版本以及有关持久性低或高的值上次更改的时间的信息。

基本上,配料结构体存储“静态数据” —— 就像记忆中的值 —— 还存储“每个配料”的内容。

运行时存储“活动的、正在进行的”数据,例如哪些查询在栈上,和/或当前活动的查询访问的依赖项。