authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-14 21:49:00+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-15 01:38:09+01:00
logc518593e9793a2aed4e0173348aff2cbef58b717
tree0c400f51aaccda7466b931a85bca4ed11936c18b
parent4df6119335513af5b8d7dc0f6421b42cf13c3654

std.Io.Threaded: spurious unparks are possible

Apparently the thread parking APIs on Windows and NetBSD aren't as good as I thought---or, at least, the way they're *used* makes them not as good. It's perfectly possible to use these APIs in a way where you don't trigger spurious wakeups, but standard primitives (SRWLOCK on Windows, pthread bits on NetBSD) are perfectly happy to leave pending unparks sitting around, meaning in practice you have to assume spurious unparks are possible. This brings me great sadness... but we soldier on!

1 files changed, 122 insertions(+), 122 deletions(-)

lib/std/Io/Threaded.zig+122-122
......@@ -3963,10 +3963,7 @@ pub fn dirOpenFileWtf16(
39633963 // kernel bug with retry attempts.
39643964 syscall.finish();
39653965 if (max_attempts - attempt == 0) return error.SharingViolation;
3966 try parking_sleep.sleep(.{ .duration = .{
3967 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3968 .clock = .awake,
3969 } });
3966 try parking_sleep.windowsRetrySleep((@as(u32, 1) << attempt) >> 1);
39703967 attempt += 1;
39713968 syscall = try .start();
39723969 continue;
......@@ -3988,10 +3985,7 @@ pub fn dirOpenFileWtf16(
39883985 // fixed by sleeping and retrying until the error goes away.
39893986 syscall.finish();
39903987 if (max_attempts - attempt == 0) return error.SharingViolation;
3991 try parking_sleep.sleep(.{ .duration = .{
3992 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3993 .clock = .awake,
3994 } });
3988 try parking_sleep.windowsRetrySleep((@as(u32, 1) << attempt) >> 1);
39953989 attempt += 1;
39963990 syscall = try .start();
39973991 continue;
......@@ -9638,6 +9632,9 @@ fn nowPosix(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
96389632fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
96399633 const t: *Threaded = @ptrCast(@alignCast(userdata));
96409634 _ = t;
9635 return nowInner(clock);
9636}
9637fn nowInner(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
96419638 return switch (native_os) {
96429639 .windows => nowWindows(clock),
96439640 .wasi => nowWasi(clock),
......@@ -9687,7 +9684,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
96879684
96889685fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
96899686 const t: *Threaded = @ptrCast(@alignCast(userdata));
9690 if (use_parking_sleep) return parking_sleep.sleep(timeout);
9687 if (use_parking_sleep) return parking_sleep.sleep(try timeout.toDeadline(ioBasic(t)));
96919688 if (native_os == .wasi) return sleepWasi(t, timeout);
96929689 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);
96939690 return sleepNanosleep(t, timeout);
......@@ -14197,10 +14194,7 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
1419714194 // this other than retrying the creation after the OS finishes
1419814195 // the deletion.
1419914196 syscall.finish();
14200 try parking_sleep.sleep(.{ .duration = .{
14201 .raw = .fromMilliseconds(1),
14202 .clock = .awake,
14203 } });
14197 try parking_sleep.windowsRetrySleep(1);
1420414198 syscall = try .start();
1420514199 continue;
1420614200 },
......@@ -15570,9 +15564,13 @@ const parking_futex = struct {
1557015564 ///
1557115565 /// * Removing the `Waiter` from `Bucket.waiters`
1557215566 /// * Decrementing `Bucket.num_waiters`
15573 /// * Unparking the thread (*after* the above, so that the `Waiter` does not go out of scope
15574 /// while it is still in the `Bucket`).
15567 /// * Atomically setting `done` (after this, the `Waiter` may go out of scope at any time,
15568 /// so must not be referenced again)
15569 /// * Unparking the thread (last, so that the unparked thread definitely sees `done`)
1557515570 thread_status: *std.atomic.Value(Thread.Status),
15571 /// Initially `false`. Whoever updates `thread_status` to `.none`/`.canceling` will update
15572 /// this to `true` once they are done with the `Waiter`, just before unparking `tid`.
15573 done: std.atomic.Value(bool),
1557615574 };
1557715575
1557815576 fn bucketForAddress(address: usize) *Bucket {
......@@ -15611,6 +15609,7 @@ const parking_futex = struct {
1561115609 .address = @intFromPtr(ptr),
1561215610 .tid = self_tid,
1561315611 .thread_status = undefined, // populated in critical section
15612 .done = .init(false),
1561415613 };
1561515614
1561615615 var status_buf: std.atomic.Value(Thread.Status) = undefined;
......@@ -15667,40 +15666,43 @@ const parking_futex = struct {
1566715666 bucket.waiters.append(&waiter.node);
1566815667 }
1566915668
15670 if (park(timeout, ptr)) {
15671 // We were unparked by either `wake` or cancelation, so our current status is either
15672 // `.none` or `.canceling`. In either case, they've already removed `waiter` from
15673 // `bucket`, so we have nothing more to do!
15669 const deadline: ?Io.Clock.Timestamp = switch (timeout) {
15670 .none => null,
15671 .duration => |d| .{
15672 .raw = (nowInner(d.clock) catch unreachable).addDuration(d.raw),
15673 .clock = d.clock,
15674 },
15675 .deadline => |d| d,
15676 };
15677 while (park(deadline, ptr)) {
15678 if (waiter.done.load(.acquire)) return; // all done!
1567415679 } else |err| switch (err) {
15675 error.Timeout => {
15676 // We're not out of the woods yet: an unpark could race with the timeout.
15677 const old_status = waiter.thread_status.fetchAnd(
15678 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
15679 .monotonic,
15680 );
15681 switch (old_status.cancelation) {
15682 .parked => {
15683 // No race. It is our responsibility to remove `waiter` from `bucket`.
15684 // New status is `.none`.
15685 bucket.mutex.lock();
15686 defer bucket.mutex.unlock();
15687 bucket.waiters.remove(&waiter.node);
15688 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
15689 },
15690 .none, .canceling => {
15691 // Race condition: the timeout was reached, then `wake` or a canceler tried
15692 // to unpark us. Whoever did that will remove us from `bucket`. Wait for
15693 // that (and drop the unpark request in doing so).
15694 // New status is `.none` or `.canceling` respectively.
15695 park(.none, ptr) catch |e| switch (e) {
15680 error.Timeout => switch (waiter.thread_status.fetchAnd(
15681 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
15682 .monotonic,
15683 ).cancelation) {
15684 .parked => {
15685 // We saw a timeout and updated our own status from `.parked` to `.none`. It is
15686 // our responsibility to remove `waiter` from `bucket`.
15687 bucket.mutex.lock();
15688 defer bucket.mutex.unlock();
15689 bucket.waiters.remove(&waiter.node);
15690 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
15691 },
15692 .none, .canceling => {
15693 // Race condition: the timeout was reached, then `wake` or a cancelation tried
15694 // to update our status. They won the race, so wait for them to do the cleanup.
15695 // They'll tell us by setting `waiter.done` and unparking us.
15696 while (!waiter.done.load(.acquire)) {
15697 park(null, ptr) catch |e| switch (e) {
1569615698 error.Timeout => unreachable,
1569715699 };
15698 },
15699 .canceled => unreachable,
15700 .blocked => unreachable,
15701 .blocked_windows_dns => unreachable,
15702 .blocked_canceling => unreachable,
15703 }
15700 }
15701 },
15702 .canceled => unreachable,
15703 .blocked => unreachable,
15704 .blocked_windows_dns => unreachable,
15705 .blocked_canceling => unreachable,
1570415706 },
1570515707 }
1570615708 }
......@@ -15748,9 +15750,6 @@ const parking_futex = struct {
1574815750 waiter.node.next = waking_head;
1574915751 waking_head = &waiter.node;
1575015752 num_removed += 1;
15751 // Signal to `waiter` that they're about to be unparked, in case we're racing with their
15752 // timeout. See corresponding logic in `wake`.
15753 waiter.address = 0;
1575415753 }
1575515754
1575615755 _ = bucket.num_waiters.fetchSub(num_removed, .monotonic);
......@@ -15765,6 +15764,8 @@ const parking_futex = struct {
1576515764 const waiter: *Waiter = @fieldParentPtr("node", node);
1576615765 unpark_buf[unpark_len] = waiter.tid;
1576715766 unpark_len += 1;
15767 waiter.done.store(true, .release);
15768 // `waiter.*` is now potentially invalid so must not be referenced again.
1576815769 if (unpark_len == unpark_buf.len) {
1576915770 unpark(&unpark_buf, ptr);
1577015771 unpark_len = 0;
......@@ -15781,13 +15782,14 @@ const parking_futex = struct {
1578115782 defer bucket.mutex.unlock();
1578215783 bucket.waiters.remove(&waiter.node);
1578315784 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
15785 waiter.done.store(true, .release); // potentially invalidates `waiter.*`
1578415786 }
1578515787};
1578615788const parking_sleep = struct {
1578715789 comptime {
1578815790 assert(use_parking_sleep);
1578915791 }
15790 fn sleep(timeout: Io.Timeout) Io.Cancelable!void {
15792 fn sleep(deadline: ?Io.Clock.Timestamp) Io.SleepError!void {
1579115793 const opt_thread = Thread.current;
1579215794 cancelable: {
1579315795 const thread = opt_thread orelse break :cancelable;
......@@ -15796,85 +15798,90 @@ const parking_sleep = struct {
1579615798 .unblocked => {},
1579715799 }
1579815800 thread.futex_waiter = null;
15799 {
15800 const old_status = thread.status.fetchOr(
15801 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },
15802 .release, // release `thread.futex_waiter`
15803 );
15804 switch (old_status.cancelation) {
15805 .none => {}, // status is now `.parked`
15806 .canceling => return error.Canceled, // status is now `.canceled`
15807 .canceled => break :cancelable, // status is still `.canceled`
15808 .parked => unreachable,
15801 const orig_status = thread.status.fetchOr(
15802 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },
15803 .release, // release `thread.futex_waiter`
15804 );
15805 switch (orig_status.cancelation) {
15806 .none => {}, // status is now `.parked`
15807 .canceling => return error.Canceled, // status is now `.canceled`
15808 .canceled => break :cancelable, // status is still `.canceled`
15809 .parked => unreachable,
15810 .blocked => unreachable,
15811 .blocked_windows_dns => unreachable,
15812 .blocked_canceling => unreachable,
15813 }
15814 while (park(deadline, null)) {
15815 // Either a cancelation or a spurious unpark; let's see which!
15816 switch (thread.status.load(.monotonic).cancelation) {
15817 .parked => continue, // spurious unpark; keep sleeping
15818 .canceling => {
15819 // We got canceled; update our state and return.
15820 thread.status.store(
15821 .{ .cancelation = .canceled, .awaitable = orig_status.awaitable },
15822 .monotonic,
15823 );
15824 return error.Canceled;
15825 },
15826 .none => unreachable,
15827 .canceled => unreachable,
1580915828 .blocked => unreachable,
1581015829 .blocked_windows_dns => unreachable,
1581115830 .blocked_canceling => unreachable,
1581215831 }
15813 }
15814 if (park(timeout, null)) {
15815 // The only reason this could possibly happen is cancelation.
15816 const old_status = thread.status.load(.monotonic);
15817 assert(old_status.cancelation == .canceling);
15818 thread.status.store(
15819 .{ .cancelation = .canceled, .awaitable = old_status.awaitable },
15820 .monotonic,
15821 );
15822 return error.Canceled;
1582315832 } else |err| switch (err) {
15824 error.Timeout => {
15825 // We're not out of the woods yet: an unpark could race with the timeout.
15826 const old_status = thread.status.fetchAnd(
15827 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
15828 .monotonic,
15829 );
15830 switch (old_status.cancelation) {
15831 .parked => return, // No race; new status is `.none`
15832 .canceling => {
15833 // Race condition: the timeout was reached, then someone tried to unpark
15834 // us for a cancelation. Whoever did that will have called `unpark`, so
15835 // drop that unpark request by waiting for it.
15836 // Status is still `.canceling`.
15837 park(.none, null) catch |e| switch (e) {
15838 error.Timeout => unreachable,
15839 };
15840 return;
15841 },
15842 .none => unreachable,
15843 .canceled => unreachable,
15844 .blocked => unreachable,
15845 .blocked_windows_dns => unreachable,
15846 .blocked_canceling => unreachable,
15847 }
15833 error.Timeout => switch (thread.status.fetchAnd(
15834 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
15835 .monotonic,
15836 ).cancelation) {
15837 // We updated our own status from `.parked` to `.none`.
15838 .parked => return, // new status is `.none`
15839 .canceling => {
15840 // Timeout raced with a cancelation. We don't need to do anything, but
15841 // the next `park` on this thread will see a spurious unpark.
15842 // Status is still `.canceling`.
15843 return;
15844 },
15845 .none => unreachable,
15846 .canceled => unreachable,
15847 .blocked => unreachable,
15848 .blocked_windows_dns => unreachable,
15849 .blocked_canceling => unreachable,
1584815850 },
1584915851 }
1585015852 }
15851 // Uncancelable sleep; we expect not to be manually unparked.
15852 if (park(timeout, null)) {
15853 unreachable; // unexpected unpark
15853 // Uncancelable sleep; this case is very simple.
15854 while (park(deadline, null)) {
15855 // Definitely spurious; nothing to do.
1585415856 } else |err| switch (err) {
1585515857 error.Timeout => return,
1585615858 }
1585715859 }
15860 /// Sleep for approximately `ms` awake milliseconds in an attempt to work around Windows kernel bugs.
15861 fn windowsRetrySleep(ms: u32) (Io.Cancelable || Io.UnexpectedError)!void {
15862 const now_timestamp = nowWindows(.awake) catch unreachable; // '.awake' is supported on Windows
15863 const deadline = now_timestamp.addDuration(.fromMilliseconds(ms));
15864 parking_sleep.sleep(.{ .raw = deadline, .clock = .awake }) catch |err| switch (err) {
15865 error.UnsupportedClock => unreachable,
15866 else => |e| return e,
15867 };
15868 }
1585815869};
1585915870
15871/// Spurious wakeups are possible.
15872///
1586015873/// `addr_hint` has no semantic effect, but may allow the OS to optimize this operation.
15861fn park(timeout: Io.Timeout, addr_hint: ?*const anyopaque) error{Timeout}!void {
15874fn park(opt_deadline: ?std.Io.Clock.Timestamp, addr_hint: ?*const anyopaque) error{Timeout}!void {
1586215875 comptime assert(use_parking_futex or use_parking_sleep);
1586315876 switch (builtin.target.os.tag) {
1586415877 .windows => {
1586515878 var timeout_buf: windows.LARGE_INTEGER = undefined;
15866 const raw_timeout: ?*windows.LARGE_INTEGER = timeout: switch (timeout) {
15867 .none => null,
15868 .deadline => |timestamp| continue :timeout .{ .duration = .{
15869 .clock = timestamp.clock,
15870 .raw = (nowWindows(timestamp.clock) catch unreachable).durationTo(timestamp.raw),
15871 } },
15872 .duration => |duration| {
15873 _ = duration.clock; // Windows only supports monotonic
15874 timeout_buf = @intCast(@divTrunc(-duration.raw.nanoseconds, 100));
15875 break :timeout &timeout_buf;
15876 },
15877 };
15879 const raw_timeout: ?*windows.LARGE_INTEGER = if (opt_deadline) |deadline| timeout: {
15880 const now_timestamp = nowWindows(deadline.clock) catch unreachable;
15881 const nanoseconds = now_timestamp.durationTo(deadline.raw).nanoseconds;
15882 timeout_buf = @intCast(@divTrunc(-nanoseconds, 100));
15883 break :timeout &timeout_buf;
15884 } else null;
1587815885 // `RtlWaitOnAddress` passes the futex address in as the first argument to this call,
1587915886 // but it's unclear what that actually does, especially since `NtAlertThreadByThreadId`
1588015887 // does *not* accept the address so the kernel can't really be using it as a hint. An
......@@ -15896,20 +15903,13 @@ fn park(timeout: Io.Timeout, addr_hint: ?*const anyopaque) error{Timeout}!void {
1589615903 },
1589715904 .netbsd => {
1589815905 var ts_buf: posix.timespec = undefined;
15899 const ts: ?*posix.timespec, const abstime: bool, const clock_real: bool = switch (timeout) {
15900 .none => .{ null, false, false },
15901 .deadline => |timestamp| timeout: {
15902 ts_buf = timestampToPosix(timestamp.raw.nanoseconds);
15903 break :timeout .{ &ts_buf, true, timestamp.clock == .real };
15904 },
15905 .duration => |duration| timeout: {
15906 ts_buf = timestampToPosix(duration.raw.nanoseconds);
15907 break :timeout .{ &ts_buf, false, duration.clock == .real };
15908 },
15909 };
15906 const ts: ?*posix.timespec, const clock_real: bool = if (opt_deadline) |deadline| timeout: {
15907 ts_buf = timestampToPosix(deadline.raw.nanoseconds);
15908 break :timeout .{ &ts_buf, deadline.clock == .real };
15909 } else .{ null, true };
1591015910 switch (posix.errno(std.c._lwp_park(
1591115911 if (clock_real) .REALTIME else .MONOTONIC,
15912 .{ .ABSTIME = abstime },
15912 .{ .ABSTIME = true },
1591315913 ts,
1591415914 0,
1591515915 addr_hint,