feature. See also
. The project being documented here (as the example) is the Zig library itself.
Threaded.ParkingMutex
const ParkingMutex = struct
File
Code
const ParkingMutex = struct {
state: std.atomic.Value(State),
const init: ParkingMutex = .{ .state = .init(.unlocked) };
comptime {
assert(use_parking_futex);
}
const State = enum(usize) {
unlocked = 1,
locked_once = 0,
_,
fn waiter(s: State) ?*Waiter {
return @ptrFromInt(@backingInt(s));
}
fn fromWaiter(w: ?*Waiter) State {
return @fromBackingInt(@intCast(@intFromPtr(w)));
}
};
const Waiter = struct {
unpark_flag: UnparkFlag,
next: ?*Waiter,
tid: std.Thread.Id,
};
fn lock(m: *ParkingMutex) void {
state: switch (State.unlocked) {
.unlocked => continue :state m.state.cmpxchgWeak(
.unlocked,
.locked_once,
.acquire,
.monotonic,
) orelse {
@branchHint(.likely);
return;
},
.locked_once, _ => |last_state| {
const old_waiter = last_state.waiter();
const self_tid = if (Thread.current) |t| t.id else std.Thread.getCurrentId();
var waiter: Waiter = .{
.next = old_waiter,
.unpark_flag = unpark_flag_init,
.tid = self_tid,
};
if (m.state.cmpxchgWeak(
.fromWaiter(old_waiter),
.fromWaiter(&waiter),
.release,
.monotonic,
)) |new_state| {
continue :state new_state;
}
park(.none, m, if (need_unpark_flag) &waiter.unpark_flag) catch |err| switch (err) {
error.Timeout => unreachable,
};
return;
},
}
}
fn unlock(m: *ParkingMutex) void {
state: switch (State.locked_once) {
.unlocked => unreachable,
.locked_once => continue :state m.state.cmpxchgWeak(
.locked_once,
.unlocked,
.release,
.acquire,
) orelse {
@branchHint(.likely);
return;
},
_ => |last_state| {
// because `Waiter.next` is owned by the lock holder (that's us!) once the waiter is
// in the linked list, up until we unpark the waiter.
// Run through the waiter list to the end to ensure fairness. This is obviously not
// ideal, but it shouldn't be a big deal in practice provided the critical section
// is fairly small (so we won't get too many threads contending the mutex at once).
// There's a *chance* we could get away with a LIFO queue for our use case, but I
// don't wanna risk that.
var parent: ?*Waiter = null;
var waiter: *Waiter = last_state.waiter().?;
while (waiter.next) |next| {
parent = waiter;
waiter = next;
}
if (parent) |p| {
assert(p.next == waiter);
p.next = null;
} else {
if (m.state.cmpxchgWeak(
.fromWaiter(last_state.waiter().?),
.locked_once,
.acquire,
.acquire,
)) |new_state| {
continue :state new_state;
}
}
const tid = waiter.tid;
if (need_unpark_flag) setUnparkFlag(&waiter.unpark_flag);
unpark(&.{tid}, m);
return;
},
}
}
}