Skip to content

Commit

Permalink
Add some documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
jimblandy committed Apr 15, 2024
1 parent 9a52fa8 commit b7de28c
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 19 deletions.
14 changes: 8 additions & 6 deletions wgpu-core/src/lock/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/*! Wrappers for locks that enforce proper acquisition order.
/*! Deadlock prevention via lock ordering.
This module defines wrappers for the lock types used in `wgpu`, `Mutex`,
`RwLock`, and `SnatchLock`, that use runtime checks to ensure that locks are
always acquired in a consistent order, ensuring that deadlock is impossible.
These checks are enabled only in debug builds, and when the `"validate-locks"`
feature is enabled.
This module defines wrappers for the lock types used in `wgpu`
(`Mutex`, `RwLock`, and `SnatchLock`) that use runtime checks to
ensure that locks are always acquired in a consistent order, ensuring
that deadlock is impossible. These checks are enabled only in debug
builds, and when the `"validate-locks"` feature is enabled.
See the [`validating`] module for details on how/why the validation works.
*/

Expand Down
46 changes: 34 additions & 12 deletions wgpu-core/src/lock/rank.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
/// A locking rank.
//! Ranks for `wgpu-core` locks, restricting acquisition order.
//!
//! See [`LockRank`].
/// The rank of a lock.
///
/// Each `Mutex`, `RwLock`, and `SnatchLock` in `wgpu-core` has been
/// assigned a *rank*: a node in the DAG defined at the bottom of
/// `wgpu-core/src/lock/rank.rs`. The rank of the most recently
/// acquired lock you are still holding determines which locks you may
/// attempt to acquire next.
///
/// When you create a lock in `wgpu-core`, you must specify its rank
/// by passing in a [`LockRank`] value. This module declares a
/// pre-defined set of ranks to cover everything in `wgpu-core`, named
/// after the type in which they occur, and the name of the type's
/// field that is a lock. For example, [`CommandBuffer::data`] is a
/// `Mutex`, and its rank here is the constant
/// [`COMMAND_BUFFER_DATA`].
///
/// [`CommandBuffer::data`]: crate::command::CommandBuffer::data
#[derive(Debug, Copy, Clone)]
pub struct LockRank {
/// The bit representing this rank.
/// The bit representing this lock.
///
/// There should only be a single bit set in this value.
pub(super) bit: LockRankSet,

/// A bitmask of permitted successor ranks.
///
/// If the most recently acquired lock we are still holding is
/// `prev`, then `prev.followers` is the rank mask of locks
/// we are allowed to acquire next.
/// If `rank` is the rank of the most recently acquired lock we
/// are still holding, then `rank.followers` is the mask of
/// locks we are allowed to acquire next.
///
/// The `define_lock_ranks!` macro ensures that there are no
/// cycles in the graph of lock ranks and their followers.
pub(super) followers: LockRankSet,
}

/// Define a set of lock ranks, and permitted successors.
/// Define a set of lock ranks, and each rank's permitted successors.
macro_rules! define_lock_ranks {
{
$(
$( #[ $attr:meta ] )*
rank $name:ident followed by { $( $follower:ident ),* $(,)? };
)*
} => {
// An enum that assigns a unique number to each rank.
#[allow(non_camel_case_types)]
#[repr(u32)]
enum LockRankNumber {
$(
$name,
)*
}
enum LockRankNumber { $( $name, )* }

bitflags::bitflags! {
#[derive(Debug, Copy, Clone)]
Expand All @@ -42,6 +63,7 @@ macro_rules! define_lock_ranks {
// If there is any cycle in the ranking, the initializers
// for `followers` will be cyclic, and rustc will give us
// an error message explaining the cycle.
$( #[ $attr ] )*
pub const $name: LockRank = LockRank {
bit: LockRankSet:: $name,
followers: LockRankSet::empty() $( .union($follower.bit) )*,
Expand Down
51 changes: 50 additions & 1 deletion wgpu-core/src/lock/validating.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,51 @@
/*! Methods that validate lock acquisition order. */
/*! Methods that validate lock acquisition order.
The [`LockRank`] constants in the [`lock::rank`] module describe edges in an
directed graph of lock acquisitions: each lock's rank says, if this is the most
recently acquired lock that you are still holding, then these are the locks you
are allowed to acquire next.
As long as this graph doesn't have cycles, any number of threads can acquire
locks along paths through the graph without deadlock:
- Assume that if a thread acquires a lock, then it will eventually either try to
acquire another lock, or release one it holds. No thread just sits on its
locks forever for unrelated reasons. If it did, then that would be a source of
deadlock "outside the system" that we can't do anything about.
- This module asserts that threads acquire and release locks in a stack-like
order: a lock is dropped only when it is the most recently acquired lock still
held - call this the "youngest" lock. This stack-like ordering isn't a Rust
requirement; Rust lets you drop guards in any order you like. This is a
restriction we impose.
- Consider the directed graph whose nodes are locks, and whose edges go from
each lock to its permitted followers, the locks in its [`LockRank::followers`]
set. The definition of the [`lock::rank`] module's `LockRank` constants
ensures that this graph has no cycles, including trivial cycles from a node to
itself.
- This module then asserts that each thread attempts to acquire a lock only if
it is among its youngest lock's permitted followers. Thus, as a thread
acquires locks, it must be traversing a path through the graph along its
edges.
- Because there are no cycles in the graph, whenever one thread is blocked
waiting to acquire a lock, that lock must be held by a different thread: if
you were allowed to acquire a lock you already hold, that would be a cycle in
the graph.
- Furthermore, because the graph has no cycles, as we work our way from each
thread to the thread it is blocked waiting for, we must eventually reach an
end point: there must be some thread that is able to acquire its next lock, or
that is about to release a lock.
Thus, the system as a whose is always able to make progress: it is free of
deadlocks.
[`lock::rank`]: crate::lock::rank
*/

#![allow(dead_code)]

use super::rank::{LockRank, LockRankSet};
Expand All @@ -25,6 +72,8 @@ struct LockState {
last_acquired: Option<&'static Location<'static>>,

/// The number of locks currently held.
///
/// This is used to enforce stack-like lock acquisition and release.
depth: u32,
}

Expand Down

0 comments on commit b7de28c

Please sign in to comment.