-
Notifications
You must be signed in to change notification settings - Fork 171
Description
Using dashmap in an async context has a high risk of accidentally deadlocking by awaiting while holding a reference into the hashmap.
Demo Project
main.rs
use std::sync::Arc;
use dashmap::DashMap;
async fn sleep() {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
async fn other_task(map: Arc<DashMap<u8, u8>>) {
println!("other task locks");
let mut n = map.get_mut(&0).unwrap();
*n += 1;
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let map = Arc::new(DashMap::<u8, u8>::default());
map.insert(0, 0);
tokio::task::spawn(other_task(map.clone()));
println!("main task locks");
let mut n = map.get_mut(&0).unwrap();
sleep().await;
*n += 1;
println!("Hello, world!");
}
Cargo.toml
[package]
name = "lint"
version = "0.1.0"
edition = "2024"
[dependencies]
dashmap = "6.1.0"
tokio = { version = "1.46.1", features = [
"macros",
"rt",
"rt-multi-thread",
"time",
] }
Such code deadlocks with no warnings:
➜ cargo run
[... updates and compiles ...]
Running `target/debug/lint`
main task locks
other task locks
^C
Thankfully, there's a lint attribute in rust nightly that prevents marked structs from being held through an action that might suspend (e.g. .await
) called #[must_not_suspend]
.
Sadly is still in nightly but It would be very useful to have this as an opt-in unstable feature to help catch potential deadlocks in projects that are on nightly.
I took the time to clone the repo and with just the final patch it's possible to make the demo code warn about the deadlock.
I'm not sure about the name of the feature and probably should be added to "multiple" Ref
s too (not sure as I haven't read about those at all).
Demo Project Changes
Add to main.rs
#![feature(must_not_suspend)]
#![warn(must_not_suspend)]
And the output finally is:
Compiling lint v0.1.0 (/tmp/lint)
warning: `dashmap::mapref::one::RefMut` held across a suspend point, but should not be
--> src/main.rs:25:9
|
25 | let mut n = map.get_mut(&0).unwrap();
| ^^^^^
26 | sleep().await;
| ----- the value is held across this suspend point
|
help: consider using a block (`{ ... }`) to shrink the value's scope, ending before the suspend point
--> src/main.rs:25:9
|
25 | let mut n = map.get_mut(&0).unwrap();
| ^^^^^
note: the lint level is defined here
--> src/main.rs:2:9
|
2 | #![warn(must_not_suspend)]
| ^^^^^^^^^^^^^^^^
warning: `lint` (bin "lint") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/lint`
main task locks
other task locks
EDIT: If someone really wants to use this I've made a fork of the project implementing this for all types of Ref
s at https://github.com/javalsai/dashmap-async.
diff --git a/Cargo.toml b/Cargo.toml
index 946e9e2..d072594 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ all = ["raw-api", "typesize", "serde", "rayon", "arbitrary"]
raw-api = []
typesize = ["dep:typesize"]
inline-more = ["hashbrown/inline-more"]
+unstable-must-not-suspend = []
[dependencies]
lock_api = "0.4.12"
diff --git a/src/lib.rs b/src/lib.rs
index 2a9c620..bff9859 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::type_complexity)]
+#![cfg_attr(feature = "unstable-must-not-suspend", feature(must_not_suspend))]
#[cfg(feature = "arbitrary")]
mod arbitrary;
diff --git a/src/mapref/one.rs b/src/mapref/one.rs
index faf47d9..eac11e4 100644
--- a/src/mapref/one.rs
+++ b/src/mapref/one.rs
@@ -3,6 +3,7 @@ use core::hash::Hash;
use core::ops::{Deref, DerefMut};
use std::fmt::{Debug, Formatter};
+#[cfg_attr(feature = "unstable-must-not-suspend", must_not_suspend)]
pub struct Ref<'a, K, V> {
_guard: RwLockReadGuardDetached<'a>,
k: &'a K,
@@ -74,6 +75,7 @@ impl<'a, K: Eq + Hash, V> Deref for Ref<'a, K, V> {
}
}
+#[cfg_attr(feature = "unstable-must-not-suspend", must_not_suspend)]
pub struct RefMut<'a, K, V> {
guard: RwLockWriteGuardDetached<'a>,
k: &'a K,