Tokio Mutex
To start, let's just swap out the Mutex and see what happens.
use serde::Serialize; use serde_json::Value; // We've dropped `sync::Mutex` here use std::{ collections::BTreeMap, sync::Arc, time::{Duration, Instant}, }; // This is our new import use tokio::sync::Mutex; async fn main() { let db = Arc::new(Mutex::new(Database::default())); tokio::task::spawn({ let db = db.clone(); async move { loop { let guard = db.lock().await; let value = guard.get("my-key"); tokio::time::sleep(Duration::from_secs(1)).await; dbg!(value); } } }); tokio::task::spawn(async move { let start = Instant::now(); loop { db.lock() .await .insert("my-key", &start.elapsed().as_millis()); tokio::time::sleep(Duration::from_millis(350)).await; } }) .await .unwrap(); }
That worked! But what exactly is happening? Let's add some print statements to see if we can figure out the ordering of operations.
async fn main() { let db = Arc::new(Mutex::new(Database::default())); tokio::task::spawn({ let db = db.clone(); async move { loop { println!("->read-guard"); let guard = db.lock().await; let value = guard.get("my-key"); println!("->read-sleep"); tokio::time::sleep(Duration::from_secs(1)).await; println!("<-read-sleep"); dbg!(value); println!("<-read-guard") } } }); tokio::task::spawn(async move { let start = Instant::now(); loop { println!("->write-guard"); db.lock() .await .insert("my-key", &start.elapsed().as_millis()); println!("<-write-guard"); tokio::time::sleep(Duration::from_millis(350)).await; } }) .await .unwrap(); }
When we run this, we get the following output.
->read-guard
->read-sleep
->write-guard
<-read-sleep
[src/main.rs:55:17] value = None
<-read-guard
->read-guard
<-write-guard
->read-sleep
->write-guard
<-read-sleep
[src/main.rs:55:17] value = Some(
Number(1002),
)
<-read-guard
->read-guard
<-write-guard
So, looking over our logs it seems like our get
task locks our Mutex
and then immeadly sleeps,
tokio
then selects our insert
task which attempts to lock our Mutex
however since our get
task already has the lock
it will yield back to tokio
. Once our sleep
finishes it prints out
the value and drops the guard and then immedatly tries to lock the map again. At this point because
the insert
task called lock
before our second lock
in the get task, tokio
will let our
insert
task start to make progress which inserts our first value.
Notice how the insert is delayed by the access in our get
task, In this case that would be
desirable since we don't want to be able to mutate a value when we don't have exclusive access to
it.