Can RwLock make sure my data is up-to-date?

// Stuff already in the database (cache)
// let items_map = Arc::new(RwLock::new(HashMap::new()));
// let mut write_lock = items_map.write().await
// write_lock.insert(String::from("111"), String::from("some_unique_id"));

// The new items that need to be added to the database (not in the hashmap)
// let new_items  = vec![String::from("999")];

// | Thread 1                     | Thread 2                     |
// | update(new_items, items_map) | update(new_items, items_map) |
// | ...                          | ...                          |
// | 999 not in db                | 999 not in db                |
// | write.lock()                 |                              |
// | unique_add(item)             |                              |
// | adds to hashmap              |                              |
// | drops write.lock()           | write.lock()                 |
// |                              | adds to db                   |
// |                              | (error?????)                 |

use tokio::sync::RwLock;
async fn update(new_items: Vec<String>, items_map: Arc<RwLock<HashMap<String, String>>>) {
    let items_lock = items_map.read().await;

    for item in new_items {
        // If the item is already in the map, skip it
        if items_lock.contains_key(&item) {
            continue;
        }

        let mut items_lock = items_map.write().await;
        let id = unique_add(item.clone()).await;
        items_lock.insert(item, id);
    }
}

// Function will give an error if the item is already in the database (unique constraint)
async fn unique_add(item: String) -> String {
    String::from("some_unique_id")
}

After reading that Rwlock can have more than 1 reader, I thought this would be a better idea than using a Mutex since there is a higher chance the information is already in the cache.

However, I am not sure if the above code would lead to an error since the unique_update function will give a unique constraint error if the value is already in the db. Thanks!

Have you actually run it?

It looks to me like it will deadlock because you are attempting to get a write lock while a read lock is still being held.

2 Likes

I can't really run it right now since I don't have the db functionality built. Before I built it wanted to make sure this works.

As for the deadlock, I don't think that will happen according to the tokio docs since I am reading and then writing which is dropped in the next iteration of the loop.

Note that under the priority policy of RwLock, read locks are not granted until prior write locks, to prevent starvation. Therefore deadlock may occur if a read lock is held by the current task, a write lock attempt is made, and then a subsequent read lock attempt is made by the current task.

No, your code is definitely going to deadlock. Tokio's RwLock is designed to be held across .await points and locks are only released when they are dropped. So when you try to get write access while holding a read lock, you will never get the write lock, because you never release the read lock and you can only acquire the write lock when there are no read locks present, which will never happen (because we never get to the point of dropping the read lock we already have).

I agree though, while the documentation you've linked is correct, it is confusing without an example[1]. Here's how I think the wording could be improved to illustrate the described deadlock scenario:

Note that under the priority policy of RwLock, read locks are not granted until prior write locks, to prevent starvation. Therefore deadlock may occur if a read lock is held by the current task, a write lock attempt by another task is made, and then a subsequent read lock attempt is made by the current task, while still holding the first read lock.

Something like this should work just fine (though very dangerous, as the tokio docs describe):

{
    let read1 = lock.read().await;
    let read2 = lock.read().await;
}

Even though read1 still exists, we can get a second read lock, thanks to the fact that RwLock supports multiple simultaneous readers. The problem arises when a different task tries to get a write lock between the acquisition of read1 and read2. This is because write locks have priority in order to prevent write starvation. That means that the write lock has to wait for read1 to be dropped. This will never happen, because we wait for read2 to become available which in turn waits for the write request to be granted.


  1. Says the person that wrote the paragraph above lol. ↩ī¸Ž

1 Like

Ok that makes sense. Thanks!

Then would it be best to stick with tokio::sync::Mutex in the update function where I get the lock at the beginning and then drops at the end automatically which should work and not lead to an error but will be slow since the lock is being used throughout the function

or

if a different way of using RwLock would work in the update function:

use tokio::sync::RwLock;
async fn update(new_items: Vec<String>, items_map: Arc<RwLock<HashMap<String, String>>>) {
    let not_in_db = {
        let items_lock = items_map.read().await;
        new_items
            .into_iter()
            .filter(|item| !items_lock.contains_key(item))
            .collect::<Vec<_>>()
    };

    let mut items_lock = items_map.write().await;
    for item in not_in_db {
        let id = unique_update(item.clone()).await;
        items_lock.insert(item, id);
    }
}

// Function will give an error if the item is already in the database (unique constraint)
async fn unique_update(item: String) -> String {
    String::from("some_unique_id")
}

I am assuming now that the above code won't deadlock but will most likely lead to error since if one thread gets the write lock first adds it the db and once the other thread gets it it will give a unique constraint error. If this is case can I use RwLock for this use case? Maybe using Upsert, which wouldn't give the error since if it already there it just updates it?

The difference between Mutex and RwLock is that the former only allows 1 reader or 1 writer at a time, while the latter allows multiple readers but only 1 writer at a time.

If you want to use Tokio's RwLock, just make sure that you don't hold a read lock within the same await point where you need to acquire the write lock.

Your second snippet is going in the right direction. Just make it compile.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.