Skip to content

Commit 496bf2e

Browse files
committed
chore: add docs for the global re-entrant lock
1 parent adc9f5c commit 496bf2e

File tree

1 file changed

+63
-0
lines changed

1 file changed

+63
-0
lines changed

src/lib.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,32 +160,95 @@ mod lock {
160160
use std::ptr;
161161
use std::sync::{Mutex, MutexGuard, Once};
162162

163+
/// A "Maybe" LockGuard
163164
pub struct LockGuard(Option<MutexGuard<'static, ()>>);
164165

166+
/// The global lock, lazily allocated on first use
165167
static mut LOCK: *mut Mutex<()> = ptr::null_mut();
166168
static INIT: Once = Once::new();
169+
// Whether this thread is the one that holds the lock
167170
thread_local!(static LOCK_HELD: Cell<bool> = Cell::new(false));
168171

169172
impl Drop for LockGuard {
170173
fn drop(&mut self) {
174+
// Don't do anything if we're a LockGuard(None)
171175
if self.0.is_some() {
172176
LOCK_HELD.with(|slot| {
177+
// Immediately crash if we somehow aren't the thread holding this lock
173178
assert!(slot.get());
179+
// We are no longer the thread holding this lock
174180
slot.set(false);
175181
});
176182
}
183+
// lock implicitly released here, if we're a LockGuard(Some(..))
177184
}
178185
}
179186

187+
/// Acquire a partially unsound(!!!) global re-entrant lock over
188+
/// backtrace's internals.
189+
///
190+
/// That is, this lock can be acquired as many times as you want
191+
/// on a single thread without deadlocking, allowing one thread
192+
/// to acquire exclusive access to the ability to make backtraces.
193+
/// Calls to this locking function are freely sprinkled in every place
194+
/// where that needs to be enforced.
195+
///
196+
///
197+
/// # Why
198+
///
199+
/// This was first introduced to guard uses of Windows' dbghelp API,
200+
/// which isn't threadsafe. It's unclear if other things now rely on
201+
/// this locking.
202+
///
203+
///
204+
/// # How
205+
///
206+
/// The basic idea is to have a single global mutex, and a thread_local
207+
/// boolean saying "yep this is the thread that acquired the mutex".
208+
///
209+
/// The first time a thread acquires the lock, it is handed a
210+
/// `LockGuard(Some(..))` that will actually release the lock on Drop.
211+
/// All subsequence attempts to lock on the same thread will see
212+
/// that their thread acquired the lock, and get `LockGuard(None)`
213+
/// which will do nothing when dropped.
214+
///
215+
///
216+
/// # Safety
217+
///
218+
/// As long as you only ever assign the returned LockGuard to a freshly
219+
/// declared local variable, it will do its job correctly, as the "first"
220+
/// LockGuard will strictly outlive all subsequent LockGuards and
221+
/// properly release the lock when the thread is done with backtracing.
222+
///
223+
/// However if you ever attempt to store a LockGuard beyond the scope
224+
/// it was acquired in, it might actually be a `LockGuard(None)` that
225+
/// doesn't actually hold the lock! In this case another thread might
226+
/// acquire the lock and you'll get races this system was intended to
227+
/// avoid!
228+
///
229+
/// This is why this is "partially unsound". As a public API this would
230+
/// be unacceptable, but this is crate-private, and if you use this in
231+
/// the most obvious and simplistic way it Just Works™.
232+
///
233+
/// Note however that std specifically bypasses this lock, and uses
234+
/// the `*_unsynchronized` backtrace APIs. This is "fine" because
235+
/// it wraps its own calls to backtrace in a non-reentrant Mutex
236+
/// that prevents two backtraces from getting interleaved during printing.
180237
pub fn lock() -> LockGuard {
238+
// If we're the thread holding this lock, pretend to acquire the lock
239+
// again by returning a LockGuard(None)
181240
if LOCK_HELD.with(|l| l.get()) {
182241
return LockGuard(None);
183242
}
243+
// Insist that we totally are the thread holding the lock
244+
// (our thread will block until we are)
184245
LOCK_HELD.with(|s| s.set(true));
185246
unsafe {
247+
// lazily allocate the lock if necessary
186248
INIT.call_once(|| {
187249
LOCK = Box::into_raw(Box::new(Mutex::new(())));
188250
});
251+
// ok *actually* try to acquire the lock, blocking as necessary
189252
LockGuard(Some((*LOCK).lock().unwrap()))
190253
}
191254
}

0 commit comments

Comments
 (0)