rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.81k stars 12.5k forks source link

std::sync::RwLock upgrade a ReadGuard to WriteGuard. #31670

Closed ghost closed 8 years ago

ghost commented 8 years ago

I'm writing a library implementing transactions on an memory-mapped file. Transactions can be mutable and non-mutable, with rules slightly different from the usual Rust model:

Right now, I'm using a mutex to prevent two mutable transactions at the same time, and an RwLock for the step of mutable transactions.

Using just an RwLock would require mutable transactions to first drop their ReadGuard before acquiring a WriteGuard, which means that another mutable transaction could be started at the same time.

Being able to atomically upgrade a ReadGuard into a WriteGuard would achieve the same effect with just one RwLock instead of RwLock + Mutex.

abonander commented 8 years ago

RwLock and Mutex are both wrappers for operating system primitives. Unfortunately neither Windows nor Linux seem to support upgrading a read lock to a write lock without releasing the former. If you don't release the read lock first, the thread will deadlock when you try to acquire a write lock.

ghost commented 8 years ago

(sorry about the open/close).

Sure, that's why I'm using the extra mutex. It didn't seem to me like the wrapper was that thin. If it is, then it's definitely not worth doing what I'm suggesting.

steveklabnik commented 8 years ago

/cc @rust-lang/libs , is this even possible to implement? I share @cybergeek94 's concern.

ghost commented 8 years ago

What I'm doing right now is

Readers take the read access to the RwLock, writers take the mutex (but not the RwLock). Whenever a writer needs to "upgrade", it takes the write access to RwLock.

Guarantees:

However, this is moderately nice, because all my code needs to be careful with this.

alexcrichton commented 8 years ago

Thanks for the report @pijul! As @cybergeek94 mentioned, right now we simply bind the OS primitives rather than build a suite of fancier synchronization utilities on top of them. This sort of functionality can always be done through crates.io!

That, plus an atomic rwlock upgrade is actually generally impossible. There must be a way to handle two readers upgrading simultaneously. I've heard that some other libraries handle this by readers acquiring the lock with an indicator they might upgrade, so at most one upgrade-able reader is allowed through at any one point in time.

Overall, however, this primitive wouldn't quite fit in libstd as-is today and would probably want to exist on crates.io first.

axos88 commented 4 years ago

@ghost

What I'm doing right now is

  • 1 Mutex for "read access"
  • 1 RwLock.

Readers take the read access to the RwLock, writers take the mutex (but not the RwLock). Whenever a writer needs to "upgrade", it takes the write access to RwLock.

Guarantees:

  • No race condition: because of the mutex, there is always at most one writer. in particular, during the "upgrade", no other writer can be started.
  • No deadlock: in readers, there is a single resource. In the writer, the mutex is always taken before the write lock.

However, this is moderately nice, because all my code needs to be careful with this.

Wouldn't a reader and a writer be able to access the data in this case?

jaykrell commented 2 years ago

It works better than you say, but not great. The writer taking the mutex is a potential writer. A reader that might want to write. To become a writer, it takes the rwlock for write.

d4mr commented 6 months ago

I am curious, how would upgrading a read to a write operate differently than first releasing the readlock then acquiring a writelock? Is there a situation where being able to upgrade allows more functionality?

d4mr commented 6 months ago

I am curious, how would upgrading a read to a write operate differently than first releasing the readlock then acquiring a writelock? Is there a situation where being able to upgrade allows more functionality?

Blayung commented 5 months ago

@d4mr The data might get modified between the drop call and aquiring the write lock, and we don't want that.

jaykrell commented 5 months ago

This is an interesting topic I did some research and implementation.

There is no "perfect" solution.

One good solution, I think was, internally you have the "real" lock and a "writeIntent" lock.

LockExclusive:
  writeIntent.LockExclusive()
  real.LockExclusive()
  writeIntent.Unlock()

TryConvertSharedToExclusive:
  if !writeIntent.TryLockExclusive()
    return false
  real.Unlock()
  // no writer can enter because we have writeIntent
  // readers can enter, but they will not mutate
  real.LockExclusive()
  writeIntent.Unlock()
  return true

The catch is that "upgrade" can fail. If it fails, then the caller has to, like, release its read lock, and try its larger process again. Allowing for mutations in the mean time. But the solution is good, compared to other attempts, because no mutation can occur between taking the read lock and upgrading it to write.