Overview

When developing applications with tokio one of the larger challenges is how to manage sharing access to resources that either need to be mutated or might have some internal state that needs to be protected. There are a few options for managing this synchronization. To go over this problem and what solutions are available we are going to use an somewhat contrived example of a rudimentary in-memory database. The basic structure will look something like the following.

#![allow(unused)]
fn main() {
use std::collections::BTreeMap;
use serde_json::Value;
use serde::Serialize;

/// A key value store where all keys are Strings
/// and any associated values backed by json values
#[derive(Debug, Default)]
pub struct Database {
    /// The mapping of keys to values
    map: BTreeMap<String, Value>,
}

impl Database {
    /// Get a reference to the values paired with the key if one exists
    pub fn get(&self, key: impl AsRef<str>) -> Option<&Value> {
        self.map.get(key.as_ref())
    }

    /// Insert a new value into the map, overwriting any previous value of one was present
    pub fn insert(&mut self, key: impl ToString, value: &impl Serialize) {
        let value = serde_json::to_value(value).unwrap();
        self.map.insert(key.to_string(), value);
    }
}
}

Our Database, is a simple wrapper around BTreeMap that isn't all that interesting yet but it does impose an issue if we tried to use this as a shared resource across tasks.

use std::time::{Duration, Instant};

#[tokio::main]
async fn main() {
    let mut db = Database::default();
    tokio::task::spawn(async {
        loop {
            dbg!(db.get("my-key"));
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    });

    tokio::task::spawn(async {
        let start = Instant::now();
        loop {
            db.insert("my-key", start.elapsed().as_millis())
        }
    });
}

The above will error with the following message

error[E0502]: cannot borrow `db` as mutable because it is also borrowed as immutable
  --> src/main.rs:38:24
   |
31 |        tokio::task::spawn(async {
   |  ______-__________________-
   | | _____|
   | ||
32 | ||         loop {
33 | ||             dbg!(db.get("my-key"));
   | ||                  -- first borrow occurs due to use of `db` in coroutine
34 | ||             tokio::time::sleep(Duration::from_secs(1)).await;
35 | ||         }
36 | ||     });
   | ||_____-- argument requires that `db` is borrowed for `'static`
   | |______|
   |        immutable borrow occurs here
37 |
38 |        tokio::task::spawn(async {
   |   ________________________^
39 |  |         let start = Instant::now();
40 |  |         loop {
41 |  |             db.insert("my-key", &start.elapsed().as_millis())
   |  |             -- second borrow occurs due to use of `db` in coroutine
42 |  |         }
43 |  |     });
   |  |_____^ mutable borrow

Because the task that calls get and the task that calls insert both need a reference to this shared database the compiler is complaining that we need somehting to synchronize the insert and get operations.

In the next chapter we are going to look at how we might achieve this with the standard libaray's primitives.