Rust,删除变量时锁定互斥锁会导致死锁

问题描述 投票:0回答: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);
    }
}

然而,因为记录操作还需要锁定互斥锁,所以在将变量索引传递给后端函数时我遇到了死锁。

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 Ideomatic
       ret
   }
}

我通过在获取值后手动删除变量来解决问题。然而,这似乎不太理想。

有没有一种方法可以在 Rust 中构建它,这样我就不会遇到死锁,也不必重复手动挂断电话?我想 FFI 将不得不经常处理类似的事情(因为 C 代码经常使用全局状态)这些如何解决这个问题以及 Rust 中预防死锁的一些资源是什么?

rust mutex deadlock global-state
3个回答
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
(也可以考虑使用 as
std::mem::drop
来区分),或者使用 block 表达式 来限定它的范围,如上例所示。

如果您想了解更多信息,Rust 参考的这一部分 有一些关于删除规则的更深入的细节。


0
投票

我做了一些更多的测试,因为我有点困惑为什么我在只获取对单个资源的锁时遇到了死锁。 我在没有删除的情况下发布的代码也可以工作。不起作用的是直接将结果返回给表达式。

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):

备注:

在函数体的最终表达式中创建的临时变量在函数体中绑定的任何命名变量之后被 >dropped,因为没有 >smaller 封闭的临时范围。


-1
投票

听起来你有一个典型的死锁案例,因为同时持有两个锁而导致死锁。在您的代码中,Add 实现获取 Mutex 上的锁,然后调用 drop 方法,该方法也获取相同的锁。如果另一个线程试图在第一个线程持有锁时获取锁,这可能会导致死锁。

为避免这种情况,您可以使用一种称为“锁排序”的技术来确保以一致的顺序获取锁。在您的情况下,您可以按变量的索引对锁进行排序,以便 Add 实现始终首先获取具有较低索引的变量的锁。然后,在 drop 方法中,您可以先获取索引较高的变量的锁,然后再释放索引较低的变量的锁。

这是一个示例实现:

   struct VarIndex(u32);
    
    impl VarIndex {
        fn lock<'a>(&self, ir: &'a Mutex<InternalRepresentation>) -> MutexGuard<'a, InternalRepresentation> {
            ir.lock().unwrap()
        }
    }
    
    impl Drop for Var {
        fn drop(&mut self) {
            let ir = IR.lock().unwrap();
            let self_idx = VarIndex(self.0);
            let other_idx = self_idx.0 + 1;
            let other_lock = VarIndex(other_idx).lock(&ir);
            ir.dec_ref_count(self.0);
            drop(other_lock);
        }
    }
    
    impl<T: Into<Var>> ops::Add<T> for Var {
        type Output = Var;
    
        fn add(self, rhs: T) -> Self::Output {
            let rhs = rhs.into();
            let ir = IR.lock().unwrap();
            let self_idx = VarIndex(self.0);
            let other_idx = rhs.0;
            let other_lock = VarIndex(other_idx).lock(&ir);
            let ret = Self(ir.add(self_idx.0, other_idx));
            drop(other_lock);
            ret
        }
    }

在这个实现中,VarIndex 类型用于表示一个变量索引,并提供了一种方法来获取 Mutex 上的锁。 Drop 实现在释放较低索引变量的锁之前获取具有较高索引的变量的锁。 Add 实现还在调用后端函数之前以正确的顺序获取锁。

关于 Rust 中防止死锁的资源,Rust Programming Language 书有一章关于并发和并行性,深入介绍了该主题。还有几个 crate 为 Rust 中的并发提供更高级别的抽象,例如 crossbeam 和 tokio,它们内置了避免死锁的支持。

© www.soinside.com 2019 - 2024. All rights reserved.