Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Adopt the Tracked Primitives

Deloxide sees synchronization only when the code uses its tracked wrappers. Start by replacing the locks on the path you are investigating, initialize the detector before that path runs, and expand from there. The wrappers keep the familiar guard-based style while reporting supported lock activity to the detector.

#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::{Condvar, Deloxide, Mutex, RwLock};

Deloxide::new()
    .callback(|report| eprintln!("deadlock report: {report:?}"))
    .start()
    .expect("detector initialization");

let counter = Mutex::new(0);
*counter.lock() += 1;

let settings = RwLock::new(String::from("ready"));
assert_eq!(settings.read().as_str(), "ready");
settings.write().push_str(" for work");

let ready = Condvar::new();
let _ = ready;
}

For complete item documentation, see Mutex, RwLock, Condvar, and Deloxide::start.

Replace imports, not the locking model

For selected code, replace either family of imports with Deloxide’s types:

// Before: use std::sync::{Condvar, Mutex, RwLock};
// Before: use parking_lot::{Condvar, Mutex, RwLock};
use deloxide::{Condvar, Mutex, RwLock};

This is an import diff rather than a Rust example. The runnable forms are the repository’s basic_mutex, rwlock, and condvar examples.

Mutex::lock, RwLock::read, and RwLock::write return guards directly. A guard dereferences to its protected value, and dropping it always releases the physical lock. The wrapper also reports the release globally when active tracking or logging requires it; uncontended fast paths avoid unnecessary global detector work. Keep the usual narrow scopes and explicit drop(guard) where the release point matters. The wrappers use parking_lot internally; they do not expose std::sync poisoning or LockResult/PoisonError. In particular, remove .unwrap() or poisoned-lock recovery that existed only to handle the standard library result:

#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::Mutex;

let jobs = Mutex::new(Vec::<String>::new());
jobs.lock().push("index".to_owned()); // No LockResult to unwrap.
assert_eq!(jobs.lock().len(), 1);
}

That is a semantic migration, not merely a type alias: code that relies on poisoning as an application health signal needs its own explicit failure state.

Use each wrapper with its matching guard

Mutex is for exclusive access. RwLock has distinct read and write guards: multiple reads may coexist, while a write is exclusive. A Deloxide Condvar waits with a mutable Deloxide MutexGuard; do not mix it with a std::sync::Mutex or parking_lot::Mutex guard. Its wait methods release the associated mutex while waiting and reacquire it before returning.

#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::{Condvar, Mutex};
use std::sync::Arc;

let state = Arc::new((Mutex::new(false), Condvar::new()));
let (lock, wake) = &*state;
let mut started = lock.lock();
while !*started {
    // `wait` returns with `started` holding the same tracked mutex again.
    wake.wait(&mut started);
}
}

Use the normal predicate loop: wakeups are not a reason to assume the predicate is true. The wrapper also offers Condvar::wait_timeout, wait_while, and wait_timeout_while. Unlike std::sync::Condvar, these methods mutate the supplied guard in place: wait_timeout and wait_timeout_while return bool (true means the timeout elapsed), rather than returning a guard/result pair.

There is no read-to-write upgrade method. Release a read guard before taking a write guard; attempting a blocking write while retaining a read guard can self-deadlock.

#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::RwLock;

let cache = RwLock::new(vec![1, 2]);
{
    let read = cache.read();
    assert_eq!(read.len(), 2);
} // The read guard is gone before the write attempt.
cache.write().push(3);
}

Nonblocking probes

Mutex::try_lock, RwLock::try_read, and RwLock::try_write are nonblocking. They return Option<Guard>: Some owns the acquired guard and None means that attempt could not acquire the lock immediately. They do not return a TryLockError and must not be treated as an eventual wait.

#![allow(unused)]
fn main() {
extern crate deloxide;
use deloxide::{Mutex, RwLock};

let mutex = Mutex::new(1);
if let Some(mut value) = mutex.try_lock() {
    *value += 1;
}

let config = RwLock::new(10);
let snapshot = config.try_read().map(|value| *value);
if let Some(mut value) = config.try_write() {
    *value += 1;
}
assert!(snapshot.is_some());
}

Roll out without overstating coverage

Begin with the locks shared by the suspected operations, convert every endpoint of that dependency, and reproduce the scenario. Then migrate adjacent lock families and worker entry points. A useful rollout order is:

  1. Initialize Deloxide before the instrumented workload.
  2. Convert the locks and condition variables in one coherent operation.
  3. Convert the threads that create that operation’s workers to deloxide::thread.
  4. Exercise the path and use reports to guide the next boundary.

An untracked boundary is a visibility boundary. If a participant holds a standard/parking_lot lock, waits on a different synchronization primitive, or uses a condition variable paired with an untracked mutex, Deloxide cannot form all of that dependency’s edges. A report is evidence about the tracked primitives, not proof that the rest of the process is free of deadlocks. Keep the original primitives where migration is not yet safe, but document the gap and avoid interpreting the mixed deployment as complete coverage.

For active findings, distinguish WaitForGraph (a current, validated cycle) from LockOrderViolation (a potential historical order cycle). Continue with lifecycle and callback guidance before enabling the detector in a long-running process.