Rust,在变量被丢弃时锁定互斥锁会导致死锁。

3

我希望这不算太离题,但我认为理解问题是必要的:

我目前正在使用Rust编写JIT,跟踪操作,将其编译为spirv并在GPU上执行计算(受EPFL的Dr.Jit启发很深)。我编写了一个用于记录操作的后端(支持手动引用计数)。每个变量都通过向量中的索引进行访问。现在我想为Rust编写一个前端,在其中我拥有内部表示的全局实例,并在Mutex之后。为了使用引用计数,当释放该变量时,我必须递减计数器。这需要在Variable的drop函数中锁定Mutex。

impl Drop for Var {
    fn drop(&mut self) {
        IR.lock().unwrap().dec_ref_count(self.0);
    }
}

然而,由于记录操作也需要锁定Mutex,当将变量索引传递给后端函数时,我会遇到死锁问题。

impl<T: Into<Var>> ops::Add<T> for Var {
   type Output = Var;

   fn add(self, rhs: T) -> Self::Output {
       let rhs = rhs.into();
       let ret = Self(IR.lock().unwrap().[<$bop:lower>](self.0, rhs.0));
       drop(rhs);// This is a workaround and does not seem very idiomatic
       ret
   }
}

我通过手动删除变量来解决了问题。然而,这似乎不是很惯用的方法。

有没有一种在Rust中构建结构的方式,以便我不会遇到死锁,并且不必重复手动删除调用?我想象FFI相对经常处理类似的问题(因为C代码经常使用全局状态),它们如何解决这个问题,以及有哪些关于Rust中死锁预防的资源?


1
为什么不使用原子引用计数,这样就不需要锁定互斥量了呢? - Chayim Friedman
我不知道你的数据长什么样,但是使用 Arc 比手动实现带互斥锁的引用计数 始终 更快。 - Chayim Friedman
本质上,我实现了一个有序图,但没有进行删除操作(引用计数实现不会删除元素,只是使其无效),节点作为向量中的元素。主要关注DFS迭代图的速度。另一种选择是使用嵌套的Arcs。直觉上,使用Vec似乎更快,但我不确定。 - TheOneTribble
您不需要为引用计数单独分配内存;您可以在同一块内存中自行完成(尽管这更加复杂)。 - Chayim Friedman
你知道一些关于这方面的资源吗?我很乐意学习它。 - TheOneTribble
显示剩余2条评论
2个回答

1
我进行了更多测试,因为我有点困惑,为什么在只获取单个资源的锁时遇到死锁问题。我发布的代码没有删除也可以工作。不起作用的是直接将结果返回给表达式。
impl<T: Into<Var>> ops::Add<T> for Var {
    type Output = Var;
    fn add(self, rhs: T) -> Self::Output {
        let rhs = rhs.into();
        Self(IR.lock().unwrap().add(self.0, rhs.0))
    }
}

然而,将结果保存在临时变量中或单独获取锁定都可以解决这个问题。
impl<T: Into<Var>> ops::Add<T> for Var {
    type Output = Var;
    fn add(self, rhs: T) -> Self::Output {
        let rhs = rhs.into();
        let ret = Self(IR.lock().unwrap().add(self.0, rhs.0));
        ret
    }
}

impl<T: Into<Var>> ops::Add<T> for Var {
    type Output = Var;
    fn add(self, rhs: T) -> Self::Output {
        let rhs = rhs.into();
        let mut ir = IR.lock().unwrap();
        Self(ir.add(self.0, rhs.0))
    }
}

在第一个示例中,看起来 MutexGuard 在 rhs 被丢弃后被丢弃。正如 Rust 参考手册所述(感谢你的 answer @dudebro):
引用: 注意: 在函数体的最终表达式中创建的临时变量将在函数体中绑定的任何命名变量之后丢弃,因为没有更小的封闭临时范围。

0
如果只是 Self(IR.lock().unwrap().[<$bop:lower>](self.0, rhs.into().0)) 无法工作,你可以考虑这样做:
let ret = {
    let rhs = rhs.into();
    Self(IR.lock().unwrap().[<$bop:lower>](self.0, rhs.0))
}; // rhs's scope ends here
ret

还不是很好,但由于您需要让rhs首先超出范围,如果在使用...(self.0, rhs.into().0)(我假设这是一个函数调用?)时它没有在此函数调用结束时超出范围,那么您的选择基本上是使用drop(还要考虑使用std::mem::drop进行区分),或者像上面的示例一样使用block expression进行作用域限定。

如果您想了解更多关于丢弃规则的详细信息,请参阅Rust参考手册的此部分


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接