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(...@@ -3963,10 +3963,7 @@ pub fn dirOpenFileWtf16(
3963 // kernel bug with retry attempts.3963 // kernel bug with retry attempts.
3964 syscall.finish();3964 syscall.finish();
3965 if (max_attempts - attempt == 0) return error.SharingViolation;3965 if (max_attempts - attempt == 0) return error.SharingViolation;
3966 try parking_sleep.sleep(.{ .duration = .{3966 try parking_sleep.windowsRetrySleep((@as(u32, 1) << attempt) >> 1);
3967 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3968 .clock = .awake,
3969 } });
3970 attempt += 1;3967 attempt += 1;
3971 syscall = try .start();3968 syscall = try .start();
3972 continue;3969 continue;
...@@ -3988,10 +3985,7 @@ pub fn dirOpenFileWtf16(...@@ -3988,10 +3985,7 @@ pub fn dirOpenFileWtf16(
3988 // fixed by sleeping and retrying until the error goes away.3985 // fixed by sleeping and retrying until the error goes away.
3989 syscall.finish();3986 syscall.finish();
3990 if (max_attempts - attempt == 0) return error.SharingViolation;3987 if (max_attempts - attempt == 0) return error.SharingViolation;
3991 try parking_sleep.sleep(.{ .duration = .{3988 try parking_sleep.windowsRetrySleep((@as(u32, 1) << attempt) >> 1);
3992 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
3993 .clock = .awake,
3994 } });
3995 attempt += 1;3989 attempt += 1;
3996 syscall = try .start();3990 syscall = try .start();
3997 continue;3991 continue;
...@@ -9638,6 +9632,9 @@ fn nowPosix(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {...@@ -9638,6 +9632,9 @@ fn nowPosix(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
9638fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {9632fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
9639 const t: *Threaded = @ptrCast(@alignCast(userdata));9633 const t: *Threaded = @ptrCast(@alignCast(userdata));
9640 _ = t;9634 _ = t;
9635 return nowInner(clock);
9636}
9637fn nowInner(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
9641 return switch (native_os) {9638 return switch (native_os) {
9642 .windows => nowWindows(clock),9639 .windows => nowWindows(clock),
9643 .wasi => nowWasi(clock),9640 .wasi => nowWasi(clock),
...@@ -9687,7 +9684,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {...@@ -9687,7 +9684,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
96879684
9688fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {9685fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
9689 const t: *Threaded = @ptrCast(@alignCast(userdata));9686 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)));
9691 if (native_os == .wasi) return sleepWasi(t, timeout);9688 if (native_os == .wasi) return sleepWasi(t, timeout);
9692 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);9689 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);
9693 return sleepNanosleep(t, timeout);9690 return sleepNanosleep(t, timeout);
...@@ -14197,10 +14194,7 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {...@@ -14197,10 +14194,7 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
14197 // this other than retrying the creation after the OS finishes14194 // this other than retrying the creation after the OS finishes
14198 // the deletion.14195 // the deletion.
14199 syscall.finish();14196 syscall.finish();
14200 try parking_sleep.sleep(.{ .duration = .{14197 try parking_sleep.windowsRetrySleep(1);
14201 .raw = .fromMilliseconds(1),
14202 .clock = .awake,
14203 } });
14204 syscall = try .start();14198 syscall = try .start();
14205 continue;14199 continue;
14206 },14200 },
...@@ -15570,9 +15564,13 @@ const parking_futex = struct {...@@ -15570,9 +15564,13 @@ const parking_futex = struct {
15570 ///15564 ///
15571 /// * Removing the `Waiter` from `Bucket.waiters`15565 /// * Removing the `Waiter` from `Bucket.waiters`
15572 /// * Decrementing `Bucket.num_waiters`15566 /// * Decrementing `Bucket.num_waiters`
15573 /// * Unparking the thread (*after* the above, so that the `Waiter` does not go out of scope15567 /// * Atomically setting `done` (after this, the `Waiter` may go out of scope at any time,
15574 /// while it is still in the `Bucket`).15568 /// so must not be referenced again)
15569 /// * Unparking the thread (last, so that the unparked thread definitely sees `done`)
15575 thread_status: *std.atomic.Value(Thread.Status),15570 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),
15576 };15574 };
1557715575
15578 fn bucketForAddress(address: usize) *Bucket {15576 fn bucketForAddress(address: usize) *Bucket {
...@@ -15611,6 +15609,7 @@ const parking_futex = struct {...@@ -15611,6 +15609,7 @@ const parking_futex = struct {
15611 .address = @intFromPtr(ptr),15609 .address = @intFromPtr(ptr),
15612 .tid = self_tid,15610 .tid = self_tid,
15613 .thread_status = undefined, // populated in critical section15611 .thread_status = undefined, // populated in critical section
15612 .done = .init(false),
15614 };15613 };
1561515614
15616 var status_buf: std.atomic.Value(Thread.Status) = undefined;15615 var status_buf: std.atomic.Value(Thread.Status) = undefined;
...@@ -15667,40 +15666,43 @@ const parking_futex = struct {...@@ -15667,40 +15666,43 @@ const parking_futex = struct {
15667 bucket.waiters.append(&waiter.node);15666 bucket.waiters.append(&waiter.node);
15668 }15667 }
1566915668
15670 if (park(timeout, ptr)) {15669 const deadline: ?Io.Clock.Timestamp = switch (timeout) {
15671 // We were unparked by either `wake` or cancelation, so our current status is either15670 .none => null,
15672 // `.none` or `.canceling`. In either case, they've already removed `waiter` from15671 .duration => |d| .{
15673 // `bucket`, so we have nothing more to do!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!
15674 } else |err| switch (err) {15679 } else |err| switch (err) {
15675 error.Timeout => {15680 error.Timeout => switch (waiter.thread_status.fetchAnd(
15676 // We're not out of the woods yet: an unpark could race with the timeout.15681 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
15677 const old_status = waiter.thread_status.fetchAnd(15682 .monotonic,
15678 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },15683 ).cancelation) {
15679 .monotonic,15684 .parked => {
15680 );15685 // We saw a timeout and updated our own status from `.parked` to `.none`. It is
15681 switch (old_status.cancelation) {15686 // our responsibility to remove `waiter` from `bucket`.
15682 .parked => {15687 bucket.mutex.lock();
15683 // No race. It is our responsibility to remove `waiter` from `bucket`.15688 defer bucket.mutex.unlock();
15684 // New status is `.none`.15689 bucket.waiters.remove(&waiter.node);
15685 bucket.mutex.lock();15690 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
15686 defer bucket.mutex.unlock();15691 },
15687 bucket.waiters.remove(&waiter.node);15692 .none, .canceling => {
15688 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);15693 // Race condition: the timeout was reached, then `wake` or a cancelation tried
15689 },15694 // to update our status. They won the race, so wait for them to do the cleanup.
15690 .none, .canceling => {15695 // They'll tell us by setting `waiter.done` and unparking us.
15691 // Race condition: the timeout was reached, then `wake` or a canceler tried15696 while (!waiter.done.load(.acquire)) {
15692 // to unpark us. Whoever did that will remove us from `bucket`. Wait for15697 park(null, ptr) catch |e| switch (e) {
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) {
15696 error.Timeout => unreachable,15698 error.Timeout => unreachable,
15697 };15699 };
15698 },15700 }
15699 .canceled => unreachable,15701 },
15700 .blocked => unreachable,15702 .canceled => unreachable,
15701 .blocked_windows_dns => unreachable,15703 .blocked => unreachable,
15702 .blocked_canceling => unreachable,15704 .blocked_windows_dns => unreachable,
15703 }15705 .blocked_canceling => unreachable,
15704 },15706 },
15705 }15707 }
15706 }15708 }
...@@ -15748,9 +15750,6 @@ const parking_futex = struct {...@@ -15748,9 +15750,6 @@ const parking_futex = struct {
15748 waiter.node.next = waking_head;15750 waiter.node.next = waking_head;
15749 waking_head = &waiter.node;15751 waking_head = &waiter.node;
15750 num_removed += 1;15752 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;
15754 }15753 }
1575515754
15756 _ = bucket.num_waiters.fetchSub(num_removed, .monotonic);15755 _ = bucket.num_waiters.fetchSub(num_removed, .monotonic);
...@@ -15765,6 +15764,8 @@ const parking_futex = struct {...@@ -15765,6 +15764,8 @@ const parking_futex = struct {
15765 const waiter: *Waiter = @fieldParentPtr("node", node);15764 const waiter: *Waiter = @fieldParentPtr("node", node);
15766 unpark_buf[unpark_len] = waiter.tid;15765 unpark_buf[unpark_len] = waiter.tid;
15767 unpark_len += 1;15766 unpark_len += 1;
15767 waiter.done.store(true, .release);
15768 // `waiter.*` is now potentially invalid so must not be referenced again.
15768 if (unpark_len == unpark_buf.len) {15769 if (unpark_len == unpark_buf.len) {
15769 unpark(&unpark_buf, ptr);15770 unpark(&unpark_buf, ptr);
15770 unpark_len = 0;15771 unpark_len = 0;
...@@ -15781,13 +15782,14 @@ const parking_futex = struct {...@@ -15781,13 +15782,14 @@ const parking_futex = struct {
15781 defer bucket.mutex.unlock();15782 defer bucket.mutex.unlock();
15782 bucket.waiters.remove(&waiter.node);15783 bucket.waiters.remove(&waiter.node);
15783 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);15784 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
15785 waiter.done.store(true, .release); // potentially invalidates `waiter.*`
15784 }15786 }
15785};15787};
15786const parking_sleep = struct {15788const parking_sleep = struct {
15787 comptime {15789 comptime {
15788 assert(use_parking_sleep);15790 assert(use_parking_sleep);
15789 }15791 }
15790 fn sleep(timeout: Io.Timeout) Io.Cancelable!void {15792 fn sleep(deadline: ?Io.Clock.Timestamp) Io.SleepError!void {
15791 const opt_thread = Thread.current;15793 const opt_thread = Thread.current;
15792 cancelable: {15794 cancelable: {
15793 const thread = opt_thread orelse break :cancelable;15795 const thread = opt_thread orelse break :cancelable;
...@@ -15796,85 +15798,90 @@ const parking_sleep = struct {...@@ -15796,85 +15798,90 @@ const parking_sleep = struct {
15796 .unblocked => {},15798 .unblocked => {},
15797 }15799 }
15798 thread.futex_waiter = null;15800 thread.futex_waiter = null;
15799 {15801 const orig_status = thread.status.fetchOr(
15800 const old_status = thread.status.fetchOr(15802 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },
15801 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },15803 .release, // release `thread.futex_waiter`
15802 .release, // release `thread.futex_waiter`15804 );
15803 );15805 switch (orig_status.cancelation) {
15804 switch (old_status.cancelation) {15806 .none => {}, // status is now `.parked`
15805 .none => {}, // status is now `.parked`15807 .canceling => return error.Canceled, // status is now `.canceled`
15806 .canceling => return error.Canceled, // status is now `.canceled`15808 .canceled => break :cancelable, // status is still `.canceled`
15807 .canceled => break :cancelable, // status is still `.canceled`15809 .parked => unreachable,
15808 .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,
15809 .blocked => unreachable,15828 .blocked => unreachable,
15810 .blocked_windows_dns => unreachable,15829 .blocked_windows_dns => unreachable,
15811 .blocked_canceling => unreachable,15830 .blocked_canceling => unreachable,
15812 }15831 }
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;
15823 } else |err| switch (err) {15832 } else |err| switch (err) {
15824 error.Timeout => {15833 error.Timeout => switch (thread.status.fetchAnd(
15825 // We're not out of the woods yet: an unpark could race with the timeout.15834 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
15826 const old_status = thread.status.fetchAnd(15835 .monotonic,
15827 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },15836 ).cancelation) {
15828 .monotonic,15837 // We updated our own status from `.parked` to `.none`.
15829 );15838 .parked => return, // new status is `.none`
15830 switch (old_status.cancelation) {15839 .canceling => {
15831 .parked => return, // No race; new status is `.none`15840 // Timeout raced with a cancelation. We don't need to do anything, but
15832 .canceling => {15841 // the next `park` on this thread will see a spurious unpark.
15833 // Race condition: the timeout was reached, then someone tried to unpark15842 // Status is still `.canceling`.
15834 // us for a cancelation. Whoever did that will have called `unpark`, so15843 return;
15835 // drop that unpark request by waiting for it.15844 },
15836 // Status is still `.canceling`.15845 .none => unreachable,
15837 park(.none, null) catch |e| switch (e) {15846 .canceled => unreachable,
15838 error.Timeout => unreachable,15847 .blocked => unreachable,
15839 };15848 .blocked_windows_dns => unreachable,
15840 return;15849 .blocked_canceling => unreachable,
15841 },
15842 .none => unreachable,
15843 .canceled => unreachable,
15844 .blocked => unreachable,
15845 .blocked_windows_dns => unreachable,
15846 .blocked_canceling => unreachable,
15847 }
15848 },15850 },
15849 }15851 }
15850 }15852 }
15851 // Uncancelable sleep; we expect not to be manually unparked.15853 // Uncancelable sleep; this case is very simple.
15852 if (park(timeout, null)) {15854 while (park(deadline, null)) {
15853 unreachable; // unexpected unpark15855 // Definitely spurious; nothing to do.
15854 } else |err| switch (err) {15856 } else |err| switch (err) {
15855 error.Timeout => return,15857 error.Timeout => return,
15856 }15858 }
15857 }15859 }
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 }
15858};15869};
1585915870
15871/// Spurious wakeups are possible.
15872///
15860/// `addr_hint` has no semantic effect, but may allow the OS to optimize this operation.15873/// `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 {
15862 comptime assert(use_parking_futex or use_parking_sleep);15875 comptime assert(use_parking_futex or use_parking_sleep);
15863 switch (builtin.target.os.tag) {15876 switch (builtin.target.os.tag) {
15864 .windows => {15877 .windows => {
15865 var timeout_buf: windows.LARGE_INTEGER = undefined;15878 var timeout_buf: windows.LARGE_INTEGER = undefined;
15866 const raw_timeout: ?*windows.LARGE_INTEGER = timeout: switch (timeout) {15879 const raw_timeout: ?*windows.LARGE_INTEGER = if (opt_deadline) |deadline| timeout: {
15867 .none => null,15880 const now_timestamp = nowWindows(deadline.clock) catch unreachable;
15868 .deadline => |timestamp| continue :timeout .{ .duration = .{15881 const nanoseconds = now_timestamp.durationTo(deadline.raw).nanoseconds;
15869 .clock = timestamp.clock,15882 timeout_buf = @intCast(@divTrunc(-nanoseconds, 100));
15870 .raw = (nowWindows(timestamp.clock) catch unreachable).durationTo(timestamp.raw),15883 break :timeout &timeout_buf;
15871 } },15884 } else null;
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 };
15878 // `RtlWaitOnAddress` passes the futex address in as the first argument to this call,15885 // `RtlWaitOnAddress` passes the futex address in as the first argument to this call,
15879 // but it's unclear what that actually does, especially since `NtAlertThreadByThreadId`15886 // but it's unclear what that actually does, especially since `NtAlertThreadByThreadId`
15880 // does *not* accept the address so the kernel can't really be using it as a hint. An15887 // 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 {...@@ -15896,20 +15903,13 @@ fn park(timeout: Io.Timeout, addr_hint: ?*const anyopaque) error{Timeout}!void {
15896 },15903 },
15897 .netbsd => {15904 .netbsd => {
15898 var ts_buf: posix.timespec = undefined;15905 var ts_buf: posix.timespec = undefined;
15899 const ts: ?*posix.timespec, const abstime: bool, const clock_real: bool = switch (timeout) {15906 const ts: ?*posix.timespec, const clock_real: bool = if (opt_deadline) |deadline| timeout: {
15900 .none => .{ null, false, false },15907 ts_buf = timestampToPosix(deadline.raw.nanoseconds);
15901 .deadline => |timestamp| timeout: {15908 break :timeout .{ &ts_buf, deadline.clock == .real };
15902 ts_buf = timestampToPosix(timestamp.raw.nanoseconds);15909 } else .{ null, true };
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 };
15910 switch (posix.errno(std.c._lwp_park(15910 switch (posix.errno(std.c._lwp_park(
15911 if (clock_real) .REALTIME else .MONOTONIC,15911 if (clock_real) .REALTIME else .MONOTONIC,
15912 .{ .ABSTIME = abstime },15912 .{ .ABSTIME = true },
15913 ts,15913 ts,
15914 0,15914 0,
15915 addr_hint,15915 addr_hint,