-
Notifications
You must be signed in to change notification settings - Fork 377
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a simple
FairRwLock
to avoid readers starving writers
Because we handle messages (which can take some time, persisting things to disk or validating cryptographic signatures) with the top-level read lock, but require the top-level write lock to connect new peers or handle disconnection, we are particularly sensitive to writer starvation issues. Rust's libstd RwLock does not provide any fairness guarantees, using whatever the OS provides as-is. On Linux, pthreads defaults to starving writers, which Rust's RwLock exposes to us (without any configurability). Here we work around that issue by blocking readers if there are pending writers, optimizing for readable code over perfectly-optimized blocking.
- Loading branch information
1 parent
97435c3
commit bacc578
Showing
3 changed files
with
57 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
#[cfg(feature = "std")] | ||
mod rwlock { | ||
use std::sync::{TryLockResult, LockResult, RwLock, RwLockReadGuard, RwLockWriteGuard}; | ||
use std::sync::atomic::{AtomicUsize, Ordering}; | ||
|
||
/// Rust libstd's RwLock does not provide any fairness guarantees (and, in fact, when used on | ||
/// Linux with pthreads under the hood, readers trivially and completely starve writers). | ||
/// Because we often hold read locks while doing message processing in multiple threads which | ||
/// can use significant CPU time, with write locks being time-sensitive but relatively small in | ||
/// CPU time, we can end up with starvation completely blocking incoming connections or pings, | ||
/// especially during initial graph sync. | ||
/// | ||
/// Thus, we need to block readers when a writer is pending, which we do with a trivial RwLock | ||
/// wrapper here. Its not particularly optimized, but provides some reasonable fairness by | ||
/// blocking readers (by taking the write lock) if there are writers pending when we go to take | ||
/// a read lock. | ||
pub struct FairRwLock<T> { | ||
lock: RwLock<T>, | ||
waiting_writers: AtomicUsize, | ||
} | ||
|
||
impl<T> FairRwLock<T> { | ||
pub fn new(t: T) -> Self { | ||
Self { lock: RwLock::new(t), waiting_writers: AtomicUsize::new(0) } | ||
} | ||
|
||
pub fn write(&self) -> LockResult<RwLockWriteGuard<T>> { | ||
self.waiting_writers.fetch_add(1, Ordering::AcqRel); | ||
let res = self.lock.write(); | ||
self.waiting_writers.fetch_sub(1, Ordering::AcqRel); | ||
res | ||
} | ||
|
||
pub fn try_write(&self) -> TryLockResult<RwLockWriteGuard<T>> { | ||
self.lock.try_write() | ||
} | ||
|
||
pub fn read(&self) -> LockResult<RwLockReadGuard<T>> { | ||
if self.waiting_writers.load(Ordering::Acquire) != 0 { | ||
let _write_queue_lock = self.lock.write(); | ||
} | ||
self.lock.read() | ||
} | ||
} | ||
} | ||
#[cfg(feature = "std")] | ||
pub use self::rwlock::*; | ||
|
||
#[cfg(not(feature = "std"))] | ||
pub type FairRwLock<T> = crate::sync::RwLock<T>; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters