fn wait(ptr: *const u32, expect: u32, uncancelable: bool, timeout: Io.Timeout) Io.Cancelable!void {
const bucket = bucketForAddress(@intFromPtr(ptr));
// Put the threadlocal access outside of the critical section.
const opt_thread = Thread.current;
const self_tid = getParkTid();
var waiter: Waiter = .{
.node = undefined, // populated by list append
.address = @intFromPtr(ptr),
.tid = self_tid,
.thread_status = undefined, // populated in critical section
.unpark_flag = undefined, // populated in critical section
};
var status_buf: std.atomic.Value(Thread.Status) = undefined;
var unpark_flag_buf: UnparkFlag = unpark_flag_init;
{
bucket.mutex.lock();
defer bucket.mutex.unlock();
_ = bucket.num_waiters.fetchAdd(1, .acquire);
if (@atomicLoad(u32, ptr, .monotonic) != expect) {
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
return;
}
// This is in the critical section to avoid marking the thread as parked until we're
// certain that we're actually going to park.
waiter.thread_status, waiter.unpark_flag = status: {
cancelable: {
if (uncancelable) break :cancelable;
const thread = opt_thread orelse break :cancelable;
switch (thread.cancel_protection) {
.blocked => break :cancelable,
.unblocked => {},
}
thread.futex_waiter = &waiter;
const old_status = thread.status.fetchOr(
.{ .cancelation = @fromBackingInt(@intCast(0b001)), .awaitable = .null },
.release, // release `thread.futex_waiter`
);
switch (old_status.cancelation) {
.none => {}, // status is now `.parked`
.canceling => {
// status is now `.canceled`
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
return error.Canceled;
},
.canceled => break :cancelable, // status is still `.canceled`
.parked => unreachable,
.blocked => unreachable,
.blocked_alertable => unreachable,
.blocked_alertable_canceling => unreachable,
.blocked_canceling => unreachable,
}
// We could now be unparked for a cancelation at any time!
break :status .{ &thread.status, if (need_unpark_flag) &thread.unpark_flag };
}
// This is an uncancelable wait, so just use `status_buf`. Note that the value of
// `status_buf.awaitable` is irrelevant because this is only visible to futex code,
// while only cancelation cares about `awaitable`.
status_buf.raw = .{ .cancelation = .parked, .awaitable = .null };
break :status .{ &status_buf, if (need_unpark_flag) &unpark_flag_buf };
};
bucket.waiters.append(&waiter.node);
}
if (park(timeout, ptr, waiter.unpark_flag)) {
// We were unparked by either `wake` or cancelation, so our current status is either
// `.none` or `.canceling`. In either case, they've already removed `waiter` from
// `bucket`, so we have nothing more to do!
} else |err| switch (err) {
error.Timeout => {
// We're not out of the woods yet: an unpark could race with the timeout.
const old_status = waiter.thread_status.fetchAnd(
.{ .cancelation = @fromBackingInt(@intCast(0b110)), .awaitable = .all_ones },
.monotonic,
);
switch (old_status.cancelation) {
.parked => {
// No race. It is our responsibility to remove `waiter` from `bucket`.
// New status is `.none`.
bucket.mutex.lock();
defer bucket.mutex.unlock();
bucket.waiters.remove(&waiter.node);
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
},
.none, .canceling => {
// Race condition: the timeout was reached, then `wake` or a canceler tried
// to unpark us. Whoever did that will remove us from `bucket`. Wait for
// that (and drop the unpark request in doing so).
// New status is `.none` or `.canceling` respectively.
park(.none, ptr, waiter.unpark_flag) catch |e| switch (e) {
error.Timeout => unreachable,
};
},
.canceled => unreachable,
.blocked => unreachable,
.blocked_alertable => unreachable,
.blocked_canceling => unreachable,
.blocked_alertable_canceling => unreachable,
}
},
}
}