explain our rwlock implementation (and fix a potential data race)

This commit is contained in:
Ralf Jung
2020-05-04 19:37:55 +02:00
parent 61fdd3e2be
commit 40a6b8c339

View File

@@ -22,27 +22,26 @@ impl RWLock {
pub unsafe fn read(&self) { pub unsafe fn read(&self) {
let r = libc::pthread_rwlock_rdlock(self.inner.get()); let r = libc::pthread_rwlock_rdlock(self.inner.get());
// According to the pthread_rwlock_rdlock spec, this function **may** // According to POSIX, when a thread tries to acquire this read lock
// fail with EDEADLK if a deadlock is detected. On the other hand // while it already holds the write lock
// pthread mutexes will *never* return EDEADLK if they are initialized // (or vice versa, or tries to acquire the write lock twice),
// as the "fast" kind (which ours always are). As a result, a deadlock // "the call shall either deadlock or return [EDEADLK]"
// situation may actually return from the call to pthread_rwlock_rdlock // (https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_rwlock_wrlock.html,
// instead of blocking forever (as mutexes and Windows rwlocks do). Note // https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_rwlock_rdlock.html).
// that not all unix implementations, however, will return EDEADLK for // So, in principle, all we have to do here is check `r == 0` to be sure we properly
// their rwlocks. // got the lock.
// //
// We roughly maintain the deadlocking behavior by panicking to ensure // However, (at least) glibc before version 2.25 does not conform to this spec,
// that this lock acquisition does not succeed. // and can return `r == 0` even when this thread already holds the write lock.
// // We thus check for this situation ourselves and panic when detecting that a thread
// We also check whether this lock is already write locked. This // got the write lock more than once, or got a read and a write lock.
// is only possible if it was write locked by the current thread and
// the implementation allows recursive locking. The POSIX standard
// doesn't require recursively locking a rwlock to deadlock, but we can't
// allow that because it could lead to aliasing issues.
if r == libc::EAGAIN { if r == libc::EAGAIN {
panic!("rwlock maximum reader count exceeded"); panic!("rwlock maximum reader count exceeded");
} else if r == libc::EDEADLK || (r == 0 && *self.write_locked.get()) { } else if r == libc::EDEADLK || (r == 0 && *self.write_locked.get()) {
// Above, we make sure to only access `write_locked` when `r == 0` to avoid
// data races.
if r == 0 { if r == 0 {
// `pthread_rwlock_rdlock` succeeded when it should not have.
self.raw_unlock(); self.raw_unlock();
} }
panic!("rwlock read lock would result in deadlock"); panic!("rwlock read lock would result in deadlock");
@@ -56,6 +55,7 @@ impl RWLock {
let r = libc::pthread_rwlock_tryrdlock(self.inner.get()); let r = libc::pthread_rwlock_tryrdlock(self.inner.get());
if r == 0 { if r == 0 {
if *self.write_locked.get() { if *self.write_locked.get() {
// `pthread_rwlock_tryrdlock` succeeded when it should not have.
self.raw_unlock(); self.raw_unlock();
false false
} else { } else {
@@ -69,18 +69,21 @@ impl RWLock {
#[inline] #[inline]
pub unsafe fn write(&self) { pub unsafe fn write(&self) {
let r = libc::pthread_rwlock_wrlock(self.inner.get()); let r = libc::pthread_rwlock_wrlock(self.inner.get());
// See comments above for why we check for EDEADLK and write_locked. We // See comments above for why we check for EDEADLK and write_locked. For the same reason,
// also need to check that num_readers is 0. // we also need to check that there are no readers (tracked in `num_readers`).
if r == libc::EDEADLK if r == libc::EDEADLK
|| *self.write_locked.get() || (r == 0 && *self.write_locked.get())
|| self.num_readers.load(Ordering::Relaxed) != 0 || self.num_readers.load(Ordering::Relaxed) != 0
{ {
// Above, we make sure to only access `write_locked` when `r == 0` to avoid
// data races.
if r == 0 { if r == 0 {
// `pthread_rwlock_wrlock` succeeded when it should not have.
self.raw_unlock(); self.raw_unlock();
} }
panic!("rwlock write lock would result in deadlock"); panic!("rwlock write lock would result in deadlock");
} else { } else {
debug_assert_eq!(r, 0); assert_eq!(r, 0);
} }
*self.write_locked.get() = true; *self.write_locked.get() = true;
} }
@@ -89,6 +92,7 @@ impl RWLock {
let r = libc::pthread_rwlock_trywrlock(self.inner.get()); let r = libc::pthread_rwlock_trywrlock(self.inner.get());
if r == 0 { if r == 0 {
if *self.write_locked.get() || self.num_readers.load(Ordering::Relaxed) != 0 { if *self.write_locked.get() || self.num_readers.load(Ordering::Relaxed) != 0 {
// `pthread_rwlock_trywrlock` succeeded when it should not have.
self.raw_unlock(); self.raw_unlock();
false false
} else { } else {