RwLockWriteGuard as RwLockReadGuard

I have a type similar to RwLock and I'd like to add a function like this:

impl<'a, T> RwLockWriteGuard<'a, T>
{
    pub fn as_non_mut(&self) -> RwLockReadGuard<'_, T> {}
}

The RwLockWriteGuard stays borrowed for as long as RwLockReadGuard exists which should prevent new RwLockReadGuard from being created with the RwLock.

None of the RwLock I could find have a similar function. The closest I could find are functions taking the guard by value.

Is this function safe?

Guard objects are only necessary when there is something that ought to happen when the guard object is dropped. If you start with a &RwLockWriteGuard<'_, T> borrow then that's unnecessary, as that write guard takes care of any on locking logic eventually, and the mutable (re-)borrow can be completely understood by the borrow checker alone; so you can directly get a normal &T reference with the same lifetime as the &RwLockWriteGuard.... Hence the first question here is "why do you want a guard instead of a normal reference?"

1 Like

I see, great question!

My guards have time information in addition to the usual things. So for some methods a &T isn't enough.
But that definitely explains why std and other RwLocks don't need a function like this, Deref is enough.

Here's a link to the actual guard: guard

1 Like

There are some rwlocks that have a method for downgrading the lock. For example in Tokio and parking_lot.

2 Likes

Yes it's what I was referring to with "functions taking the guard by value".
But I want to keep the write guard alive.

You're not allowed to have a read and write guard at the same time.

1 Like

That's my main question :sweat_smile:
Why is it not allowed (in this context)?

It violates the "sharing xor mutability" principle, causing memory unsafety. Just think about it: if you can read and write the "locked" value at the same time, why do you need a lock in the first place? Having both a read and a write guard at the same time is exactly as bad as having no lock around the value at all.

But I'm borrowing the write lock so I can't write with it, it's like a reborrow.

let mut foo = "foo".to_string();
    
let rm = &mut foo;
let r: &String = rm;

// here I have both a &mut and & to foo
    
dbg!(rm.len()); // can still use rm immutably but not exclusively
dbg!(r);

I don't think it's easily implementable though – the lock likely assumes that the existence of read guard(s) prevents the existence of a write guard and vice versa. Then all sorts of internal invariants regarding the number of readers and writers will not be upheld.

I can imagine wrapping the write guard so that it only hands out immutable borrows, though – that way you can add your own timing information:

struct RwLockDowngradedReadGuard<'a, 'b, T> {
    guard: &'a RwLockWriteGuard<'b, T>,
    created_at: Instant,
}

impl<'a, 'b, T> RwLockDowngradedReadGuard<'a, 'b, T> {
    fn downgrade(guard: &'a RwLockWriteGuard<'b, T>) -> Self {
        let created_at = Instant::now();
        RwLockDowngradedReadGuard { guard, created_at }
    }
}

impl<T> Drop for RwLockDowngradedReadGuard<'_, '_, T> {
    fn drop(&mut self) {
        let elapsed = Instant::now().duration_since(self.created_at);
        println!("{:?}", elapsed);
    }
}

impl<'a, 'b, T> Deref for RwLockDowngradedReadGuard<'a, 'b, T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &**self.guard
    }
}

Playground.

The goal is to be able to call a function that takes a RwLockReadGuard.
The time info would not change, the ones from the RwLockWriteGuard can be reused.

The implementation should work.
The main thing would be that either the reborrowed RwLockReadGuard can't be cloned. Or cloning a RwLockReadGuard doesn't check for write.

The read/write counter sets the highest bit for writes and increments for reads.
At the end of a read the counter decrements and for writes it sets it at 0.
So reborrow should work, it would force a read and the counter would end up at highest bit + 1.
As long as the write lock exists it would be impossible to get any new guards from the RwLock. But new reborrow could be made and maybe new clones of the shared guard.

The full context of this thread

I'm sorry I left out quite a bit of context.

It all starts with this issue Help me some lifetime issue.. · Issue #177 · leudz/shipyard · GitHub.

View and ViewMut are the read and write locks.
&View and &ViewMut both implement a Get trait with an associated type &T.
Get can also be used to get a &mut T, that's why the associated type is not T.

What the user wants should work in theory but I'm guessing the invariant lifetime on ViewMut with the lifetime on Get associated type prevent the compiler from finding a lifetime that works.

My solution to the issue is to not use impl Get but View in inner_func.
This means that you can't call it with a ViewMut, except if there is a way to get a View from a ViewMut.
But of course it seems wrong, that's why we're here :smile:

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.