Skip to content

Commit f38fd38

Browse files
Mutex deadlock detection in debug
Add a debug implementation to Mutex that detects deadlocks caused by calling lock twice in a single thread.
1 parent 116b770 commit f38fd38

File tree

2 files changed

+64
-22
lines changed

2 files changed

+64
-22
lines changed

lib/std/Thread/Condition.zig

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,20 @@ const WindowsImpl = struct {
161161
}
162162
}
163163

164+
if (comptime builtin.mode == .Debug) {
165+
// The internal state of the DebugMutex needs to be handled here as well.
166+
mutex.impl.locking_thread.store(0, .Unordered);
167+
}
164168
const rc = os.windows.kernel32.SleepConditionVariableSRW(
165169
&self.condition,
166-
&mutex.impl.srwlock,
170+
if (comptime builtin.mode == .Debug) &mutex.impl.impl.srwlock else &mutex.impl.srwlock,
167171
timeout_ms,
168172
0, // the srwlock was assumed to acquired in exclusive mode not shared
169173
);
174+
if (comptime builtin.mode == .Debug) {
175+
// The internal state of the DebugMutex needs to be handled here as well.
176+
mutex.impl.locking_thread.store(std.Thread.getCurrentId(), .Unordered);
177+
}
170178

171179
// Return error.Timeout if we know the timeout elapsed correctly.
172180
if (rc == os.windows.FALSE) {

lib/std/Thread/Mutex.zig

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ const os = std.os;
2727
const assert = std.debug.assert;
2828
const testing = std.testing;
2929
const Atomic = std.atomic.Atomic;
30-
const Futex = std.Thread.Futex;
30+
const Thread = std.Thread;
31+
const Futex = Thread.Futex;
3132

3233
impl: Impl = .{},
3334

@@ -51,7 +52,12 @@ pub fn unlock(self: *Mutex) void {
5152
self.impl.unlock();
5253
}
5354

54-
const Impl = if (builtin.single_threaded)
55+
const Impl = if (builtin.mode == .Debug and !builtin.single_threaded)
56+
DebugImpl
57+
else
58+
ReleaseImpl;
59+
60+
const ReleaseImpl = if (builtin.single_threaded)
5561
SingleThreadedImpl
5662
else if (builtin.os.tag == .windows)
5763
WindowsImpl
@@ -60,22 +66,50 @@ else if (builtin.os.tag.isDarwin())
6066
else
6167
FutexImpl;
6268

69+
const DebugImpl = struct {
70+
locking_thread: Atomic(Thread.Id) = Atomic(Thread.Id).init(0), // 0 means it's not locked.
71+
impl: ReleaseImpl = .{},
72+
73+
inline fn tryLock(self: *@This()) bool {
74+
const locking = self.impl.tryLock();
75+
if (locking) {
76+
self.locking_thread.store(Thread.getCurrentId(), .Unordered);
77+
}
78+
return locking;
79+
}
80+
81+
inline fn lock(self: *@This()) void {
82+
const current_id = Thread.getCurrentId();
83+
if (self.locking_thread.load(.Unordered) == current_id and current_id != 0) {
84+
@panic("Deadlock detected");
85+
}
86+
self.impl.lock();
87+
self.locking_thread.store(current_id, .Unordered);
88+
}
89+
90+
inline fn unlock(self: *@This()) void {
91+
assert(self.locking_thread.load(.Unordered) == Thread.getCurrentId());
92+
self.locking_thread.store(0, .Unordered);
93+
self.impl.unlock();
94+
}
95+
};
96+
6397
const SingleThreadedImpl = struct {
6498
is_locked: bool = false,
6599

66-
fn tryLock(self: *Impl) bool {
100+
fn tryLock(self: *@This()) bool {
67101
if (self.is_locked) return false;
68102
self.is_locked = true;
69103
return true;
70104
}
71105

72-
fn lock(self: *Impl) void {
106+
fn lock(self: *@This()) void {
73107
if (!self.tryLock()) {
74108
unreachable; // deadlock detected
75109
}
76110
}
77111

78-
fn unlock(self: *Impl) void {
112+
fn unlock(self: *@This()) void {
79113
assert(self.is_locked);
80114
self.is_locked = false;
81115
}
@@ -86,15 +120,15 @@ const SingleThreadedImpl = struct {
86120
const WindowsImpl = struct {
87121
srwlock: os.windows.SRWLOCK = .{},
88122

89-
fn tryLock(self: *Impl) bool {
123+
fn tryLock(self: *@This()) bool {
90124
return os.windows.kernel32.TryAcquireSRWLockExclusive(&self.srwlock) != os.windows.FALSE;
91125
}
92126

93-
fn lock(self: *Impl) void {
127+
fn lock(self: *@This()) void {
94128
os.windows.kernel32.AcquireSRWLockExclusive(&self.srwlock);
95129
}
96130

97-
fn unlock(self: *Impl) void {
131+
fn unlock(self: *@This()) void {
98132
os.windows.kernel32.ReleaseSRWLockExclusive(&self.srwlock);
99133
}
100134
};
@@ -103,15 +137,15 @@ const WindowsImpl = struct {
103137
const DarwinImpl = struct {
104138
oul: os.darwin.os_unfair_lock = .{},
105139

106-
fn tryLock(self: *Impl) bool {
140+
fn tryLock(self: *@This()) bool {
107141
return os.darwin.os_unfair_lock_trylock(&self.oul);
108142
}
109143

110-
fn lock(self: *Impl) void {
144+
fn lock(self: *@This()) void {
111145
os.darwin.os_unfair_lock_lock(&self.oul);
112146
}
113147

114-
fn unlock(self: *Impl) void {
148+
fn unlock(self: *@This()) void {
115149
os.darwin.os_unfair_lock_unlock(&self.oul);
116150
}
117151
};
@@ -123,19 +157,19 @@ const FutexImpl = struct {
123157
const locked = 0b01;
124158
const contended = 0b11; // must contain the `locked` bit for x86 optimization below
125159

126-
fn tryLock(self: *Impl) bool {
160+
fn tryLock(self: *@This()) bool {
127161
// Lock with compareAndSwap instead of tryCompareAndSwap to avoid reporting spurious CAS failure.
128162
return self.lockFast("compareAndSwap");
129163
}
130164

131-
fn lock(self: *Impl) void {
165+
fn lock(self: *@This()) void {
132166
// Lock with tryCompareAndSwap instead of compareAndSwap due to being more inline-able on LL/SC archs like ARM.
133167
if (!self.lockFast("tryCompareAndSwap")) {
134168
self.lockSlow();
135169
}
136170
}
137171

138-
inline fn lockFast(self: *Impl, comptime casFn: []const u8) bool {
172+
inline fn lockFast(self: *@This(), comptime casFn: []const u8) bool {
139173
// On x86, use `lock bts` instead of `lock cmpxchg` as:
140174
// - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048
141175
// - `lock bts` is smaller instruction-wise which makes it better for inlining
@@ -149,7 +183,7 @@ const FutexImpl = struct {
149183
return @field(self.state, casFn)(unlocked, locked, .Acquire, .Monotonic) == null;
150184
}
151185

152-
fn lockSlow(self: *Impl) void {
186+
fn lockSlow(self: *@This()) void {
153187
@setCold(true);
154188

155189
// Avoid doing an atomic swap below if we already know the state is contended.
@@ -172,7 +206,7 @@ const FutexImpl = struct {
172206
}
173207
}
174208

175-
fn unlock(self: *Impl) void {
209+
fn unlock(self: *@This()) void {
176210
// Unlock the mutex and wake up a waiting thread if any.
177211
//
178212
// A waiting thread will acquire with `contended` instead of `locked`
@@ -228,7 +262,7 @@ test "Mutex - many uncontended" {
228262

229263
const Runner = struct {
230264
mutex: Mutex = .{},
231-
thread: std.Thread = undefined,
265+
thread: Thread = undefined,
232266
counter: NonAtomicCounter = .{},
233267

234268
fn run(self: *@This()) void {
@@ -243,7 +277,7 @@ test "Mutex - many uncontended" {
243277
};
244278

245279
var runners = [_]Runner{.{}} ** num_threads;
246-
for (runners) |*r| r.thread = try std.Thread.spawn(.{}, Runner.run, .{r});
280+
for (runners) |*r| r.thread = try Thread.spawn(.{}, Runner.run, .{r});
247281
for (runners) |r| r.thread.join();
248282
for (runners) |r| try testing.expectEqual(r.counter.get(), num_increments);
249283
}
@@ -265,7 +299,7 @@ test "Mutex - many contended" {
265299
var i: usize = num_increments;
266300
while (i > 0) : (i -= 1) {
267301
// Occasionally hint to let another thread run.
268-
defer if (i % 100 == 0) std.Thread.yield() catch {};
302+
defer if (i % 100 == 0) Thread.yield() catch {};
269303

270304
self.mutex.lock();
271305
defer self.mutex.unlock();
@@ -277,8 +311,8 @@ test "Mutex - many contended" {
277311

278312
var runner = Runner{};
279313

280-
var threads: [num_threads]std.Thread = undefined;
281-
for (threads) |*t| t.* = try std.Thread.spawn(.{}, Runner.run, .{&runner});
314+
var threads: [num_threads]Thread = undefined;
315+
for (threads) |*t| t.* = try Thread.spawn(.{}, Runner.run, .{&runner});
282316
for (threads) |t| t.join();
283317

284318
try testing.expectEqual(runner.counter.get(), num_increments * num_threads);

0 commit comments

Comments
 (0)