authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-02-03 19:50:31+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-02-03 22:47:19+00:00
log7c08f77efa6052750118881b506f7b59918993e0
tree709e8cd8f846c98d59d47b3712e942889d4aa280
parent56a43fb86f5127171709033fb43e19bc3b8a1cdc
signaturelock-open Commit is signed but in an unrecognized format.

Revert "std.Io.Threaded: spurious unparks are possible"

It turns out that at least on Windows, spurious unparks are *not* possible, and in fact triggering them breaks some RTL synchronization primitives. For instance, if you have a pending unpark going into a contended `RtlEnterCriticalSection` call, it will never unblock. In other words, the Windows API worked exactly how I thought it did, and it's only the NetBSD/Illumos one which is dumb. This is actually exactly why Windows 8 introduced the parking API despite alertable sleeps being a thing! This commit doesn't yet deal with making NetBSD work, nor does it even compile I imagine. The next commit will fix everything back up. This reverts commit c518593e9793a2aed4e0173348aff2cbef58b717.

1 files changed, 123 insertions(+), 121 deletions(-)

lib/std/Io/Threaded.zig+123-121
...@@ -4955,7 +4955,10 @@ pub fn dirOpenFileWtf16(...@@ -4955,7 +4955,10 @@ pub fn dirOpenFileWtf16(
4955 // kernel bug with retry attempts.4955 // kernel bug with retry attempts.
4956 syscall.finish();4956 syscall.finish();
4957 if (max_attempts - attempt == 0) return error.FileBusy;4957 if (max_attempts - attempt == 0) return error.FileBusy;
4958 try parking_sleep.windowsRetrySleep((@as(u32, 1) << attempt) >> 1);4958 try parking_sleep.sleep(.{ .duration = .{
4959 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
4960 .clock = .awake,
4961 } });
4959 attempt += 1;4962 attempt += 1;
4960 syscall = try .start();4963 syscall = try .start();
4961 continue;4964 continue;
...@@ -4977,7 +4980,10 @@ pub fn dirOpenFileWtf16(...@@ -4977,7 +4980,10 @@ pub fn dirOpenFileWtf16(
4977 // fixed by sleeping and retrying until the error goes away.4980 // fixed by sleeping and retrying until the error goes away.
4978 syscall.finish();4981 syscall.finish();
4979 if (max_attempts - attempt == 0) return error.FileBusy;4982 if (max_attempts - attempt == 0) return error.FileBusy;
4980 try parking_sleep.windowsRetrySleep((@as(u32, 1) << attempt) >> 1);4983 try parking_sleep.sleep(.{ .duration = .{
4984 .raw = .fromMilliseconds((@as(u32, 1) << attempt) >> 1),
4985 .clock = .awake,
4986 } });
4981 attempt += 1;4987 attempt += 1;
4982 syscall = try .start();4988 syscall = try .start();
4983 continue;4989 continue;
...@@ -10823,9 +10829,6 @@ fn nowPosix(clock: Io.Clock) Io.Timestamp {...@@ -10823,9 +10829,6 @@ fn nowPosix(clock: Io.Clock) Io.Timestamp {
10823fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Timestamp {10829fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Timestamp {
10824 const t: *Threaded = @ptrCast(@alignCast(userdata));10830 const t: *Threaded = @ptrCast(@alignCast(userdata));
10825 _ = t;10831 _ = t;
10826 return nowInner(clock);
10827}
10828fn nowInner(clock: Io.Clock) Io.Timestamp {
10829 return switch (native_os) {10832 return switch (native_os) {
10830 .windows => nowWindows(clock),10833 .windows => nowWindows(clock),
10831 .wasi => nowWasi(clock),10834 .wasi => nowWasi(clock),
...@@ -15582,7 +15585,10 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {...@@ -15582,7 +15585,10 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
15582 // this other than retrying the creation after the OS finishes15585 // this other than retrying the creation after the OS finishes
15583 // the deletion.15586 // the deletion.
15584 syscall.finish();15587 syscall.finish();
15585 try parking_sleep.windowsRetrySleep(1);15588 try parking_sleep.sleep(.{ .duration = .{
15589 .raw = .fromMilliseconds(1),
15590 .clock = .awake,
15591 } });
15586 syscall = try .start();15592 syscall = try .start();
15587 continue;15593 continue;
15588 },15594 },
...@@ -16955,13 +16961,9 @@ const parking_futex = struct {...@@ -16955,13 +16961,9 @@ const parking_futex = struct {
16955 ///16961 ///
16956 /// * Removing the `Waiter` from `Bucket.waiters`16962 /// * Removing the `Waiter` from `Bucket.waiters`
16957 /// * Decrementing `Bucket.num_waiters`16963 /// * Decrementing `Bucket.num_waiters`
16958 /// * Atomically setting `done` (after this, the `Waiter` may go out of scope at any time,16964 /// * Unparking the thread (*after* the above, so that the `Waiter` does not go out of scope
16959 /// so must not be referenced again)16965 /// while it is still in the `Bucket`).
16960 /// * Unparking the thread (last, so that the unparked thread definitely sees `done`)
16961 thread_status: *std.atomic.Value(Thread.Status),16966 thread_status: *std.atomic.Value(Thread.Status),
16962 /// Initially `false`. Whoever updates `thread_status` to `.none`/`.canceling` will update
16963 /// this to `true` once they are done with the `Waiter`, just before unparking `tid`.
16964 done: std.atomic.Value(bool),
16965 };16967 };
1696616968
16967 fn bucketForAddress(address: usize) *Bucket {16969 fn bucketForAddress(address: usize) *Bucket {
...@@ -17000,7 +17002,6 @@ const parking_futex = struct {...@@ -17000,7 +17002,6 @@ const parking_futex = struct {
17000 .address = @intFromPtr(ptr),17002 .address = @intFromPtr(ptr),
17001 .tid = self_tid,17003 .tid = self_tid,
17002 .thread_status = undefined, // populated in critical section17004 .thread_status = undefined, // populated in critical section
17003 .done = .init(false),
17004 };17005 };
1700517006
17006 var status_buf: std.atomic.Value(Thread.Status) = undefined;17007 var status_buf: std.atomic.Value(Thread.Status) = undefined;
...@@ -17058,44 +17059,41 @@ const parking_futex = struct {...@@ -17058,44 +17059,41 @@ const parking_futex = struct {
17058 bucket.waiters.append(&waiter.node);17059 bucket.waiters.append(&waiter.node);
17059 }17060 }
1706017061
17061 const deadline: ?Io.Clock.Timestamp = switch (timeout) {17062 if (park(timeout, ptr)) {
17062 .none => null,17063 // We were unparked by either `wake` or cancelation, so our current status is either
17063 .duration => |d| .{17064 // `.none` or `.canceling`. In either case, they've already removed `waiter` from
17064 .raw = nowInner(d.clock).addDuration(d.raw),17065 // `bucket`, so we have nothing more to do!
17065 .clock = d.clock,
17066 },
17067 .deadline => |d| d,
17068 };
17069 while (park(deadline, ptr)) {
17070 if (waiter.done.load(.acquire)) return; // all done!
17071 } else |err| switch (err) {17066 } else |err| switch (err) {
17072 error.Timeout => switch (waiter.thread_status.fetchAnd(17067 error.Timeout => {
17073 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },17068 // We're not out of the woods yet: an unpark could race with the timeout.
17074 .monotonic,17069 const old_status = waiter.thread_status.fetchAnd(
17075 ).cancelation) {17070 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
17076 .parked => {17071 .monotonic,
17077 // We saw a timeout and updated our own status from `.parked` to `.none`. It is17072 );
17078 // our responsibility to remove `waiter` from `bucket`.17073 switch (old_status.cancelation) {
17079 mutexLock(&bucket.mutex);17074 .parked => {
17080 defer mutexUnlock(&bucket.mutex);17075 // No race. It is our responsibility to remove `waiter` from `bucket`.
17081 bucket.waiters.remove(&waiter.node);17076 // New status is `.none`.
17082 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);17077 bucket.mutex.lock();
17083 },17078 defer bucket.mutex.unlock();
17084 .none, .canceling => {17079 bucket.waiters.remove(&waiter.node);
17085 // Race condition: the timeout was reached, then `wake` or a cancelation tried17080 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
17086 // to update our status. They won the race, so wait for them to do the cleanup.17081 },
17087 // They'll tell us by setting `waiter.done` and unparking us.17082 .none, .canceling => {
17088 while (!waiter.done.load(.acquire)) {17083 // Race condition: the timeout was reached, then `wake` or a canceler tried
17089 park(null, ptr) catch |e| switch (e) {17084 // to unpark us. Whoever did that will remove us from `bucket`. Wait for
17085 // that (and drop the unpark request in doing so).
17086 // New status is `.none` or `.canceling` respectively.
17087 park(.none, ptr) catch |e| switch (e) {
17090 error.Timeout => unreachable,17088 error.Timeout => unreachable,
17091 };17089 };
17092 }17090 },
17093 },17091 .canceled => unreachable,
17094 .canceled => unreachable,17092 .blocked => unreachable,
17095 .blocked => unreachable,17093 .blocked_alertable => unreachable,
17096 .blocked_alertable => unreachable,17094 .blocked_canceling => unreachable,
17097 .blocked_alertable_canceling => unreachable,17095 .blocked_alertable_canceling => unreachable,
17098 .blocked_canceling => unreachable,17096 }
17099 },17097 },
17100 }17098 }
17101 }17099 }
...@@ -17144,6 +17142,9 @@ const parking_futex = struct {...@@ -17144,6 +17142,9 @@ const parking_futex = struct {
17144 waiter.node.next = waking_head;17142 waiter.node.next = waking_head;
17145 waking_head = &waiter.node;17143 waking_head = &waiter.node;
17146 num_removed += 1;17144 num_removed += 1;
17145 // Signal to `waiter` that they're about to be unparked, in case we're racing with their
17146 // timeout. See corresponding logic in `wake`.
17147 waiter.address = 0;
17147 }17148 }
1714817149
17149 _ = bucket.num_waiters.fetchSub(num_removed, .monotonic);17150 _ = bucket.num_waiters.fetchSub(num_removed, .monotonic);
...@@ -17158,8 +17159,6 @@ const parking_futex = struct {...@@ -17158,8 +17159,6 @@ const parking_futex = struct {
17158 const waiter: *Waiter = @fieldParentPtr("node", node);17159 const waiter: *Waiter = @fieldParentPtr("node", node);
17159 unpark_buf[unpark_len] = waiter.tid;17160 unpark_buf[unpark_len] = waiter.tid;
17160 unpark_len += 1;17161 unpark_len += 1;
17161 waiter.done.store(true, .release);
17162 // `waiter.*` is now potentially invalid so must not be referenced again.
17163 if (unpark_len == unpark_buf.len) {17162 if (unpark_len == unpark_buf.len) {
17164 unpark(&unpark_buf, ptr);17163 unpark(&unpark_buf, ptr);
17165 unpark_len = 0;17164 unpark_len = 0;
...@@ -17176,14 +17175,13 @@ const parking_futex = struct {...@@ -17176,14 +17175,13 @@ const parking_futex = struct {
17176 defer mutexUnlock(&bucket.mutex);17175 defer mutexUnlock(&bucket.mutex);
17177 bucket.waiters.remove(&waiter.node);17176 bucket.waiters.remove(&waiter.node);
17178 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);17177 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
17179 waiter.done.store(true, .release); // potentially invalidates `waiter.*`
17180 }17178 }
17181};17179};
17182const parking_sleep = struct {17180const parking_sleep = struct {
17183 comptime {17181 comptime {
17184 assert(use_parking_sleep);17182 assert(use_parking_sleep);
17185 }17183 }
17186 fn sleep(deadline: ?Io.Clock.Timestamp) Io.Cancelable!void {17184 fn sleep(timeout: Io.Timeout) Io.Cancelable!void {
17187 const opt_thread = Thread.current;17185 const opt_thread = Thread.current;
17188 cancelable: {17186 cancelable: {
17189 const thread = opt_thread orelse break :cancelable;17187 const thread = opt_thread orelse break :cancelable;
...@@ -17192,90 +17190,87 @@ const parking_sleep = struct {...@@ -17192,90 +17190,87 @@ const parking_sleep = struct {
17192 .unblocked => {},17190 .unblocked => {},
17193 }17191 }
17194 thread.futex_waiter = null;17192 thread.futex_waiter = null;
17195 const orig_status = thread.status.fetchOr(17193 {
17196 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },17194 const old_status = thread.status.fetchOr(
17197 .release, // release `thread.futex_waiter`17195 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },
17198 );17196 .release, // release `thread.futex_waiter`
17199 switch (orig_status.cancelation) {17197 );
17200 .none => {}, // status is now `.parked`17198 switch (old_status.cancelation) {
17201 .canceling => return error.Canceled, // status is now `.canceled`17199 .none => {}, // status is now `.parked`
17202 .canceled => break :cancelable, // status is still `.canceled`17200 .canceling => return error.Canceled, // status is now `.canceled`
17203 .parked => unreachable,17201 .canceled => break :cancelable, // status is still `.canceled`
17204 .blocked => unreachable,17202 .parked => unreachable,
17205 .blocked_alertable => unreachable,
17206 .blocked_alertable_canceling => unreachable,
17207 .blocked_canceling => unreachable,
17208 }
17209 while (park(deadline, null)) {
17210 // Either a cancelation or a spurious unpark; let's see which!
17211 switch (thread.status.load(.monotonic).cancelation) {
17212 .parked => continue, // spurious unpark; keep sleeping
17213 .canceling => {
17214 // We got canceled; update our state and return.
17215 thread.status.store(
17216 .{ .cancelation = .canceled, .awaitable = orig_status.awaitable },
17217 .monotonic,
17218 );
17219 return error.Canceled;
17220 },
17221 .none => unreachable,
17222 .canceled => unreachable,
17223 .blocked => unreachable,17203 .blocked => unreachable,
17224 .blocked_alertable => unreachable,17204 .blocked_alertable => unreachable,
17225 .blocked_alertable_canceling => unreachable,17205 .blocked_alertable_canceling => unreachable,
17226 .blocked_canceling => unreachable,17206 .blocked_canceling => unreachable,
17227 }17207 }
17228 } else |err| switch (err) {17208 }
17229 error.Timeout => switch (thread.status.fetchAnd(17209 if (park(timeout, null)) {
17230 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },17210 // The only reason this could possibly happen is cancelation.
17211 const old_status = thread.status.load(.monotonic);
17212 assert(old_status.cancelation == .canceling);
17213 thread.status.store(
17214 .{ .cancelation = .canceled, .awaitable = old_status.awaitable },
17231 .monotonic,17215 .monotonic,
17232 ).cancelation) {17216 );
17233 // We updated our own status from `.parked` to `.none`.17217 return error.Canceled;
17234 .parked => return, // new status is `.none`17218 } else |err| switch (err) {
17235 .canceling => {17219 error.Timeout => {
17236 // Timeout raced with a cancelation. We don't need to do anything, but17220 // We're not out of the woods yet: an unpark could race with the timeout.
17237 // the next `park` on this thread will see a spurious unpark.17221 const old_status = thread.status.fetchAnd(
17238 // Status is still `.canceling`.17222 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
17239 return;17223 .monotonic,
17240 },17224 );
17241 .none => unreachable,17225 switch (old_status.cancelation) {
17242 .canceled => unreachable,17226 .parked => return, // No race; new status is `.none`
17243 .blocked => unreachable,17227 .canceling => {
17244 .blocked_alertable => unreachable,17228 // Race condition: the timeout was reached, then someone tried to unpark
17245 .blocked_alertable_canceling => unreachable,17229 // us for a cancelation. Whoever did that will have called `unpark`, so
17246 .blocked_canceling => unreachable,17230 // drop that unpark request by waiting for it.
17231 // Status is still `.canceling`.
17232 park(.none, null) catch |e| switch (e) {
17233 error.Timeout => unreachable,
17234 };
17235 return;
17236 },
17237 .none => unreachable,
17238 .canceled => unreachable,
17239 .blocked => unreachable,
17240 .blocked_alertable => unreachable,
17241 .blocked_canceling => unreachable,
17242 .blocked_alertable_canceling => unreachable,
17243 }
17247 },17244 },
17248 }17245 }
17249 }17246 }
17250 // Uncancelable sleep; this case is very simple.17247 // Uncancelable sleep; we expect not to be manually unparked.
17251 while (park(deadline, null)) {17248 if (park(timeout, null)) {
17252 // Definitely spurious; nothing to do.17249 unreachable; // unexpected unpark
17253 } else |err| switch (err) {17250 } else |err| switch (err) {
17254 error.Timeout => return,17251 error.Timeout => return,
17255 }17252 }
17256 }17253 }
17257 /// Sleep for approximately `ms` awake milliseconds in an attempt to work around Windows kernel bugs.
17258 fn windowsRetrySleep(ms: u32) (Io.Cancelable || Io.UnexpectedError)!void {
17259 const now_timestamp = nowWindows(.awake); // '.awake' is supported on Windows
17260 const deadline = now_timestamp.addDuration(.fromMilliseconds(ms));
17261 try parking_sleep.sleep(.{ .raw = deadline, .clock = .awake });
17262 }
17263};17254};
1726417255
17265/// Spurious wakeups are possible.
17266///
17267/// `addr_hint` has no semantic effect, but may allow the OS to optimize this operation.17256/// `addr_hint` has no semantic effect, but may allow the OS to optimize this operation.
17268fn park(opt_deadline: ?Io.Clock.Timestamp, addr_hint: ?*const anyopaque) error{Timeout}!void {17257fn park(timeout: Io.Timeout, addr_hint: ?*const anyopaque) error{Timeout}!void {
17269 comptime assert(use_parking_futex or use_parking_sleep);17258 comptime assert(use_parking_futex or use_parking_sleep);
17270 switch (native_os) {17259 switch (native_os) {
17271 .windows => {17260 .windows => {
17272 var timeout_buf: windows.LARGE_INTEGER = undefined;17261 var timeout_buf: windows.LARGE_INTEGER = undefined;
17273 const raw_timeout: ?*windows.LARGE_INTEGER = if (opt_deadline) |deadline| timeout: {17262 const raw_timeout: ?*windows.LARGE_INTEGER = timeout: switch (timeout) {
17274 const now_timestamp = nowWindows(deadline.clock);17263 .none => null,
17275 const nanoseconds = now_timestamp.durationTo(deadline.raw).nanoseconds;17264 .deadline => |timestamp| continue :timeout .{ .duration = .{
17276 timeout_buf = @intCast(@divTrunc(-nanoseconds, 100));17265 .clock = timestamp.clock,
17277 break :timeout &timeout_buf;17266 .raw = (nowWindows(timestamp.clock) catch unreachable).durationTo(timestamp.raw),
17278 } else null;17267 } },
17268 .duration => |duration| {
17269 _ = duration.clock; // Windows only supports monotonic
17270 timeout_buf = @intCast(@divTrunc(-duration.raw.nanoseconds, 100));
17271 break :timeout &timeout_buf;
17272 },
17273 };
17279 // `RtlWaitOnAddress` passes the futex address in as the first argument to this call,17274 // `RtlWaitOnAddress` passes the futex address in as the first argument to this call,
17280 // but it's unclear what that actually does, especially since `NtAlertThreadByThreadId`17275 // but it's unclear what that actually does, especially since `NtAlertThreadByThreadId`
17281 // does *not* accept the address so the kernel can't really be using it as a hint. An17276 // does *not* accept the address so the kernel can't really be using it as a hint. An
...@@ -17297,13 +17292,20 @@ fn park(opt_deadline: ?Io.Clock.Timestamp, addr_hint: ?*const anyopaque) error{T...@@ -17297,13 +17292,20 @@ fn park(opt_deadline: ?Io.Clock.Timestamp, addr_hint: ?*const anyopaque) error{T
17297 },17292 },
17298 .netbsd => {17293 .netbsd => {
17299 var ts_buf: posix.timespec = undefined;17294 var ts_buf: posix.timespec = undefined;
17300 const ts: ?*posix.timespec, const clock_real: bool = if (opt_deadline) |deadline| timeout: {17295 const ts: ?*posix.timespec, const abstime: bool, const clock_real: bool = switch (timeout) {
17301 ts_buf = timestampToPosix(deadline.raw.nanoseconds);17296 .none => .{ null, false, false },
17302 break :timeout .{ &ts_buf, deadline.clock == .real };17297 .deadline => |timestamp| timeout: {
17303 } else .{ null, true };17298 ts_buf = timestampToPosix(timestamp.raw.nanoseconds);
17299 break :timeout .{ &ts_buf, true, timestamp.clock == .real };
17300 },
17301 .duration => |duration| timeout: {
17302 ts_buf = timestampToPosix(duration.raw.nanoseconds);
17303 break :timeout .{ &ts_buf, false, duration.clock == .real };
17304 },
17305 };
17304 switch (posix.errno(std.c._lwp_park(17306 switch (posix.errno(std.c._lwp_park(
17305 if (clock_real) .REALTIME else .MONOTONIC,17307 if (clock_real) .REALTIME else .MONOTONIC,
17306 .{ .ABSTIME = true },17308 .{ .ABSTIME = abstime },
17307 ts,17309 ts,
17308 0,17310 0,
17309 addr_hint,17311 addr_hint,