At some point in the past few years I wrote something that uses an std::sync::RwLock
in async code. In the pursuit of premature optimization and in fear of potential deadlocks, I wrote the following code:
pub async fn yield_if_would_block<T>(
r: impl Fn() -> TryLockResult<T>,
) -> Result<T, PoisonError<T>> {
loop {
match r() {
Ok(val) => break Ok(val),
Err(e) => match e {
TryLockError::Poisoned(e) => break Err(e),
TryLockError::WouldBlock => tokio::task::yield_now().await,
},
}
}
}
And then I read and write from the lock like this:
let read_lock = yield_if_would_block(|| lock.try_read()).await?;
let mut write_lock = yield_if_would_block(|| lock.try_write()).await?;
The intent was that this would help prevent deadlocks since if another task is currently holding a write lock and this code tries to read then it'll yield_now
, giving that other task a chance to finish and release the lock.
My understanding is this is basically the same as what read
does, but where I yield, read
just makes that thread sit and wait, and blocking in an async task is of course forbidden.
I'm still not holding these locks across awaits. If I understand correctly, with a single-threaded executor, that should be enough to ensure that it wouldn't ever block, since only one task will execute at a time and it won't start working on another task until it hits an await. However, in production this code uses a multi-threaded executor.
I'm hoping to get a better understanding of how these locks work in async code in general, but I'm curious about these specific questions:
- Is my code useful? Like is it worth worrying about this at all if I'm not taking it across an await?
- Is this achieving the desired goal of preventing deadlocks? Either in general or in the case that the lock was held over an await?
- Is this basically what the async versions of RwLock are doing?
a. I'm told that the async locks come with a performance hit, but it doesn't seem like my code would be less performant than the sync try_read since I'm not doing any kind of additional checks before trying to read or write. So I assume the answer to 2. must be no and the async locks are doing something else to be actually safe - what is that?