blob: 0b6f4259fb40910e15e79816ca7122225df72dd2 [file] [log] [blame]
//! Mutexes can deadlock each other, but you can avoid this by always acquiring your locks in a
//! consistent order. This crate provides tracing to ensure that you do.
//!
//! This crate tracks a virtual "stack" of locks that the current thread holds, and whenever a new
//! lock is acquired, a dependency is created from the last lock to the new one. These dependencies
//! together form a graph. As long as that graph does not contain any cycles, your program is
//! guaranteed to never deadlock.
//!
//! # Panics
//!
//! The primary method by which this crate signals an invalid lock acquisition order is by
//! panicking. When a cycle is created in the dependency graph when acquiring a lock, the thread
//! will instead panic. This panic will not poison the underlying mutex.
//!
//! This conflicting dependency is not added to the graph, so future attempts at locking should
//! succeed as normal.
//!
//! # Structure
//!
//! Each module in this crate exposes wrappers for a specific base-mutex with dependency trakcing
//! added. For now, that is limited to [`stdsync`] which provides wrappers for the base locks in the
//! standard library. More back-ends may be added as features in the future.
//!
//! # Performance considerations
//!
//! Tracing a mutex adds overhead to certain mutex operations in order to do the required
//! bookkeeping. The following actions have the following overhead.
//!
//! - **Acquiring a lock** locks the global dependency graph temporarily to check if the new lock
//! would introduce a cyclic dependency. This crate uses the algorithm proposed in ["A Dynamic
//! Topological Sort Algorithm for Directed Acyclic Graphs" by David J. Pearce and Paul H.J.
//! Kelly][paper] to detect cycles as efficently as possible. In addition, a thread local lock set
//! is updated with the new lock.
//!
//! - **Releasing a lock** updates a thread local lock set to remove the released lock.
//!
//! - **Allocating a lock** performs an atomic update to a shared counter.
//!
//! - **Deallocating a mutex** temporarily locks the global dependency graph to remove the lock
//! entry in the dependency graph.
//!
//! These operations have been reasonably optimized, but the performance penalty may yet be too much
//! for production use. In those cases, it may be beneficial to instead use debug-only versions
//! (such as [`stdsync::DebugMutex`]) which evaluate to a tracing mutex when debug assertions are
//! enabled, and to the underlying mutex when they're not.
//!
//! [paper]: https://whileydave.com/publications/pk07_jea/
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::cell::RefCell;
use std::cell::UnsafeCell;
use std::fmt;
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::ops::Deref;
use std::ops::DerefMut;
use std::ptr;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Mutex;
use std::sync::Once;
use std::sync::PoisonError;
use lazy_static::lazy_static;
#[cfg(feature = "lockapi")]
#[cfg_attr(docsrs, doc(cfg(feature = "lockapi")))]
pub use lock_api;
#[cfg(feature = "parkinglot")]
#[cfg_attr(docsrs, doc(cfg(feature = "parkinglot")))]
pub use parking_lot;
use crate::graph::DiGraph;
mod graph;
#[cfg(feature = "lockapi")]
#[cfg_attr(docsrs, doc(cfg(feature = "lockapi")))]
pub mod lockapi;
#[cfg(feature = "parkinglot")]
#[cfg_attr(docsrs, doc(cfg(feature = "parkinglot")))]
pub mod parkinglot;
pub mod stdsync;
/// Counter for Mutex IDs. Atomic avoids the need for locking.
///
/// Should be part of the `MutexID` impl but static items are not yet a thing.
static ID_SEQUENCE: AtomicUsize = AtomicUsize::new(0);
thread_local! {
/// Stack to track which locks are held
///
/// Assuming that locks are roughly released in the reverse order in which they were acquired,
/// a stack should be more efficient to keep track of the current state than a set would be.
static HELD_LOCKS: RefCell<Vec<usize>> = RefCell::new(Vec::new());
}
lazy_static! {
static ref DEPENDENCY_GRAPH: Mutex<DiGraph<usize>> = Default::default();
}
/// Dedicated ID type for Mutexes
///
/// # Unstable
///
/// This type is currently private to prevent usage while the exact implementation is figured out,
/// but it will likely be public in the future.
struct MutexId(usize);
impl MutexId {
/// Get a new, unique, mutex ID.
///
/// This ID is guaranteed to be unique within the runtime of the program.
///
/// # Panics
///
/// This function may panic when there are no more mutex IDs available. The number of mutex ids
/// is `usize::MAX - 1` which should be plenty for most practical applications.
pub fn new() -> Self {
ID_SEQUENCE
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |id| id.checked_add(1))
.map(Self)
.expect("Mutex ID wraparound happened, results unreliable")
}
pub fn value(&self) -> usize {
self.0
}
/// Get a borrowed guard for this lock.
///
/// This method adds checks adds this Mutex ID to the dependency graph as needed, and adds the
/// lock to the list of
///
/// # Panics
///
/// This method panics if the new dependency would introduce a cycle.
pub fn get_borrowed(&self) -> BorrowedMutex {
self.mark_held();
BorrowedMutex(self)
}
/// Mark this lock as held for the purposes of dependency tracking.
///
/// # Panics
///
/// This method panics if the new dependency would introduce a cycle.
pub fn mark_held(&self) {
let creates_cycle = HELD_LOCKS.with(|locks| {
if let Some(&previous) = locks.borrow().last() {
let mut graph = get_dependency_graph();
!graph.add_edge(previous, self.value())
} else {
false
}
});
if creates_cycle {
// Panic without holding the lock to avoid needlessly poisoning it
panic!("Mutex order graph should not have cycles");
}
HELD_LOCKS.with(|locks| locks.borrow_mut().push(self.value()));
}
pub unsafe fn mark_released(&self) {
HELD_LOCKS.with(|locks| {
let mut locks = locks.borrow_mut();
for (i, &lock) in locks.iter().enumerate().rev() {
if lock == self.value() {
locks.remove(i);
return;
}
}
// Drop impls shouldn't panic but if this happens something is seriously broken.
unreachable!("Tried to drop lock for mutex {:?} but it wasn't held", self)
});
}
}
impl Default for MutexId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for MutexId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MutexID({:?})", self.0)
}
}
impl Drop for MutexId {
fn drop(&mut self) {
get_dependency_graph().remove_node(self.value());
}
}
/// `const`-compatible version of [`crate::MutexId`].
///
/// This struct can be used similarly to the normal mutex ID, but to be const-compatible its ID is
/// generated on first use. This allows it to be used as the mutex ID for mutexes with a `const`
/// constructor.
///
/// This type can be largely replaced once std::lazy gets stabilized.
struct LazyMutexId {
inner: UnsafeCell<MaybeUninit<MutexId>>,
setter: Once,
_marker: PhantomData<MutexId>,
}
impl LazyMutexId {
pub const fn new() -> Self {
Self {
inner: UnsafeCell::new(MaybeUninit::uninit()),
setter: Once::new(),
_marker: PhantomData,
}
}
}
impl fmt::Debug for LazyMutexId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.deref())
}
}
impl Default for LazyMutexId {
fn default() -> Self {
Self::new()
}
}
/// Safety: the UnsafeCell is guaranteed to only be accessed mutably from a `Once`.
unsafe impl Sync for LazyMutexId {}
impl Deref for LazyMutexId {
type Target = MutexId;
fn deref(&self) -> &Self::Target {
self.setter.call_once(|| {
// Safety: this function is only called once, so only one mutable reference should exist
// at a time.
unsafe {
*self.inner.get() = MaybeUninit::new(MutexId::new());
}
});
// Safety: after the above Once runs, there are no longer any mutable references, so we can
// hand this out safely.
//
// Explanation of this monstrosity:
//
// - Get a pointer to the data from the UnsafeCell
// - Dereference that to get a reference to the underlying MaybeUninit
// - Use as_ptr on MaybeUninit to get a pointer to the initialized MutexID
// - Dereference the pointer to turn in into a reference as intended.
//
// This should get slightly nicer once `maybe_uninit_extra` is stabilized.
unsafe { &*((*self.inner.get()).as_ptr()) }
}
}
impl Drop for LazyMutexId {
fn drop(&mut self) {
if self.setter.is_completed() {
// We have a valid mutex ID and need to drop it
// Safety: we know that this pointer is valid because the initializer has successfully run.
let mutex_id = unsafe { ptr::read((*self.inner.get()).as_ptr()) };
drop(mutex_id);
}
}
}
#[derive(Debug)]
struct BorrowedMutex<'a>(&'a MutexId);
/// Drop a lock held by the current thread.
///
/// # Panics
///
/// This function panics if the lock did not appear to be handled by this thread. If that happens,
/// that is an indication of a serious design flaw in this library.
impl<'a> Drop for BorrowedMutex<'a> {
fn drop(&mut self) {
// Safety: the only way to get a BorrowedMutex is by locking the mutex.
unsafe { self.0.mark_released() };
}
}
/// Get a reference to the current dependency graph
fn get_dependency_graph() -> impl DerefMut<Target = DiGraph<usize>> {
DEPENDENCY_GRAPH
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
#[cfg(test)]
mod tests {
use rand::seq::SliceRandom;
use rand::thread_rng;
use super::*;
#[test]
fn test_next_mutex_id() {
let initial = MutexId::new();
let next = MutexId::new();
// Can't assert N + 1 because multiple threads running tests
assert!(initial.0 < next.0);
}
#[test]
fn test_lazy_mutex_id() {
let a = LazyMutexId::new();
let b = LazyMutexId::new();
let c = LazyMutexId::new();
let mut graph = get_dependency_graph();
assert!(graph.add_edge(a.value(), b.value()));
assert!(graph.add_edge(b.value(), c.value()));
// Creating an edge c → a should fail as it introduces a cycle.
assert!(!graph.add_edge(c.value(), a.value()));
// Drop graph handle so we can drop vertices without deadlocking
drop(graph);
drop(b);
// If b's destructor correctly ran correctly we can now add an edge from c to a.
assert!(get_dependency_graph().add_edge(c.value(), a.value()));
}
/// Test creating a cycle, then panicking.
#[test]
#[should_panic]
fn test_mutex_id_conflict() {
let ids = [MutexId::new(), MutexId::new(), MutexId::new()];
for i in 0..3 {
let _first_lock = ids[i].get_borrowed();
let _second_lock = ids[(i + 1) % 3].get_borrowed();
}
}
/// Fuzz the global dependency graph by fake-acquiring lots of mutexes in a valid order.
///
/// This test generates all possible forward edges in a 100-node graph consisting of natural
/// numbers, shuffles them, then adds them to the graph. This will always be a valid directed,
/// acyclic graph because there is a trivial order (the natural numbers) but because the edges
/// are added in a random order the DiGraph will still occassionally need to reorder nodes.
#[test]
fn fuzz_mutex_id() {
const NUM_NODES: usize = 100;
let ids: Vec<MutexId> = (0..NUM_NODES).map(|_| Default::default()).collect();
let mut edges = Vec::with_capacity(NUM_NODES * NUM_NODES);
for i in 0..NUM_NODES {
for j in i..NUM_NODES {
if i != j {
edges.push((i, j));
}
}
}
edges.shuffle(&mut thread_rng());
for (x, y) in edges {
// Acquire the mutexes, smallest first to ensure a cycle-free graph
let _ignored = ids[x].get_borrowed();
let _ = ids[y].get_borrowed();
}
}
}