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(
49554955 // kernel bug with retry attempts.
49564956 syscall.finish();
49574957 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 } });
49594962 attempt += 1;
49604963 syscall = try .start();
49614964 continue;
......@@ -4977,7 +4980,10 @@ pub fn dirOpenFileWtf16(
49774980 // fixed by sleeping and retrying until the error goes away.
49784981 syscall.finish();
49794982 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 } });
49814987 attempt += 1;
49824988 syscall = try .start();
49834989 continue;
......@@ -10823,9 +10829,6 @@ fn nowPosix(clock: Io.Clock) Io.Timestamp {
1082310829fn now(userdata: ?*anyopaque, clock: Io.Clock) Io.Timestamp {
1082410830 const t: *Threaded = @ptrCast(@alignCast(userdata));
1082510831 _ = t;
10826 return nowInner(clock);
10827}
10828fn nowInner(clock: Io.Clock) Io.Timestamp {
1082910832 return switch (native_os) {
1083010833 .windows => nowWindows(clock),
1083110834 .wasi => nowWasi(clock),
......@@ -15582,7 +15585,10 @@ fn getNulHandle(t: *Threaded) !windows.HANDLE {
1558215585 // this other than retrying the creation after the OS finishes
1558315586 // the deletion.
1558415587 syscall.finish();
15585 try parking_sleep.windowsRetrySleep(1);
15588 try parking_sleep.sleep(.{ .duration = .{
15589 .raw = .fromMilliseconds(1),
15590 .clock = .awake,
15591 } });
1558615592 syscall = try .start();
1558715593 continue;
1558815594 },
......@@ -16955,13 +16961,9 @@ const parking_futex = struct {
1695516961 ///
1695616962 /// * Removing the `Waiter` from `Bucket.waiters`
1695716963 /// * Decrementing `Bucket.num_waiters`
16958 /// * Atomically setting `done` (after this, the `Waiter` may go out of scope at any time,
16959 /// so must not be referenced again)
16960 /// * Unparking the thread (last, so that the unparked thread definitely sees `done`)
16964 /// * Unparking the thread (*after* the above, so that the `Waiter` does not go out of scope
16965 /// while it is still in the `Bucket`).
1696116966 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),
1696516967 };
1696616968
1696716969 fn bucketForAddress(address: usize) *Bucket {
......@@ -17000,7 +17002,6 @@ const parking_futex = struct {
1700017002 .address = @intFromPtr(ptr),
1700117003 .tid = self_tid,
1700217004 .thread_status = undefined, // populated in critical section
17003 .done = .init(false),
1700417005 };
1700517006
1700617007 var status_buf: std.atomic.Value(Thread.Status) = undefined;
......@@ -17058,44 +17059,41 @@ const parking_futex = struct {
1705817059 bucket.waiters.append(&waiter.node);
1705917060 }
1706017061
17061 const deadline: ?Io.Clock.Timestamp = switch (timeout) {
17062 .none => null,
17063 .duration => |d| .{
17064 .raw = nowInner(d.clock).addDuration(d.raw),
17065 .clock = d.clock,
17066 },
17067 .deadline => |d| d,
17068 };
17069 while (park(deadline, ptr)) {
17070 if (waiter.done.load(.acquire)) return; // all done!
17062 if (park(timeout, ptr)) {
17063 // We were unparked by either `wake` or cancelation, so our current status is either
17064 // `.none` or `.canceling`. In either case, they've already removed `waiter` from
17065 // `bucket`, so we have nothing more to do!
1707117066 } else |err| switch (err) {
17072 error.Timeout => switch (waiter.thread_status.fetchAnd(
17073 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
17074 .monotonic,
17075 ).cancelation) {
17076 .parked => {
17077 // We saw a timeout and updated our own status from `.parked` to `.none`. It is
17078 // our responsibility to remove `waiter` from `bucket`.
17079 mutexLock(&bucket.mutex);
17080 defer mutexUnlock(&bucket.mutex);
17081 bucket.waiters.remove(&waiter.node);
17082 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
17083 },
17084 .none, .canceling => {
17085 // Race condition: the timeout was reached, then `wake` or a cancelation tried
17086 // to update our status. They won the race, so wait for them to do the cleanup.
17087 // They'll tell us by setting `waiter.done` and unparking us.
17088 while (!waiter.done.load(.acquire)) {
17089 park(null, ptr) catch |e| switch (e) {
17067 error.Timeout => {
17068 // We're not out of the woods yet: an unpark could race with the timeout.
17069 const old_status = waiter.thread_status.fetchAnd(
17070 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
17071 .monotonic,
17072 );
17073 switch (old_status.cancelation) {
17074 .parked => {
17075 // No race. It is our responsibility to remove `waiter` from `bucket`.
17076 // New status is `.none`.
17077 bucket.mutex.lock();
17078 defer bucket.mutex.unlock();
17079 bucket.waiters.remove(&waiter.node);
17080 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
17081 },
17082 .none, .canceling => {
17083 // Race condition: the timeout was reached, then `wake` or a canceler tried
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) {
1709017088 error.Timeout => unreachable,
1709117089 };
17092 }
17093 },
17094 .canceled => unreachable,
17095 .blocked => unreachable,
17096 .blocked_alertable => unreachable,
17097 .blocked_alertable_canceling => unreachable,
17098 .blocked_canceling => unreachable,
17090 },
17091 .canceled => unreachable,
17092 .blocked => unreachable,
17093 .blocked_alertable => unreachable,
17094 .blocked_canceling => unreachable,
17095 .blocked_alertable_canceling => unreachable,
17096 }
1709917097 },
1710017098 }
1710117099 }
......@@ -17144,6 +17142,9 @@ const parking_futex = struct {
1714417142 waiter.node.next = waking_head;
1714517143 waking_head = &waiter.node;
1714617144 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;
1714717148 }
1714817149
1714917150 _ = bucket.num_waiters.fetchSub(num_removed, .monotonic);
......@@ -17158,8 +17159,6 @@ const parking_futex = struct {
1715817159 const waiter: *Waiter = @fieldParentPtr("node", node);
1715917160 unpark_buf[unpark_len] = waiter.tid;
1716017161 unpark_len += 1;
17161 waiter.done.store(true, .release);
17162 // `waiter.*` is now potentially invalid so must not be referenced again.
1716317162 if (unpark_len == unpark_buf.len) {
1716417163 unpark(&unpark_buf, ptr);
1716517164 unpark_len = 0;
......@@ -17176,14 +17175,13 @@ const parking_futex = struct {
1717617175 defer mutexUnlock(&bucket.mutex);
1717717176 bucket.waiters.remove(&waiter.node);
1717817177 assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
17179 waiter.done.store(true, .release); // potentially invalidates `waiter.*`
1718017178 }
1718117179};
1718217180const parking_sleep = struct {
1718317181 comptime {
1718417182 assert(use_parking_sleep);
1718517183 }
17186 fn sleep(deadline: ?Io.Clock.Timestamp) Io.Cancelable!void {
17184 fn sleep(timeout: Io.Timeout) Io.Cancelable!void {
1718717185 const opt_thread = Thread.current;
1718817186 cancelable: {
1718917187 const thread = opt_thread orelse break :cancelable;
......@@ -17192,90 +17190,87 @@ const parking_sleep = struct {
1719217190 .unblocked => {},
1719317191 }
1719417192 thread.futex_waiter = null;
17195 const orig_status = thread.status.fetchOr(
17196 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },
17197 .release, // release `thread.futex_waiter`
17198 );
17199 switch (orig_status.cancelation) {
17200 .none => {}, // status is now `.parked`
17201 .canceling => return error.Canceled, // status is now `.canceled`
17202 .canceled => break :cancelable, // status is still `.canceled`
17203 .parked => unreachable,
17204 .blocked => 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,
17193 {
17194 const old_status = thread.status.fetchOr(
17195 .{ .cancelation = @enumFromInt(0b001), .awaitable = .null },
17196 .release, // release `thread.futex_waiter`
17197 );
17198 switch (old_status.cancelation) {
17199 .none => {}, // status is now `.parked`
17200 .canceling => return error.Canceled, // status is now `.canceled`
17201 .canceled => break :cancelable, // status is still `.canceled`
17202 .parked => unreachable,
1722317203 .blocked => unreachable,
1722417204 .blocked_alertable => unreachable,
1722517205 .blocked_alertable_canceling => unreachable,
1722617206 .blocked_canceling => unreachable,
1722717207 }
17228 } else |err| switch (err) {
17229 error.Timeout => switch (thread.status.fetchAnd(
17230 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
17208 }
17209 if (park(timeout, null)) {
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 },
1723117215 .monotonic,
17232 ).cancelation) {
17233 // We updated our own status from `.parked` to `.none`.
17234 .parked => return, // new status is `.none`
17235 .canceling => {
17236 // Timeout raced with a cancelation. We don't need to do anything, but
17237 // the next `park` on this thread will see a spurious unpark.
17238 // Status is still `.canceling`.
17239 return;
17240 },
17241 .none => unreachable,
17242 .canceled => unreachable,
17243 .blocked => unreachable,
17244 .blocked_alertable => unreachable,
17245 .blocked_alertable_canceling => unreachable,
17246 .blocked_canceling => unreachable,
17216 );
17217 return error.Canceled;
17218 } else |err| switch (err) {
17219 error.Timeout => {
17220 // We're not out of the woods yet: an unpark could race with the timeout.
17221 const old_status = thread.status.fetchAnd(
17222 .{ .cancelation = @enumFromInt(0b110), .awaitable = .all_ones },
17223 .monotonic,
17224 );
17225 switch (old_status.cancelation) {
17226 .parked => return, // No race; new status is `.none`
17227 .canceling => {
17228 // Race condition: the timeout was reached, then someone tried to unpark
17229 // us for a cancelation. Whoever did that will have called `unpark`, so
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 }
1724717244 },
1724817245 }
1724917246 }
17250 // Uncancelable sleep; this case is very simple.
17251 while (park(deadline, null)) {
17252 // Definitely spurious; nothing to do.
17247 // Uncancelable sleep; we expect not to be manually unparked.
17248 if (park(timeout, null)) {
17249 unreachable; // unexpected unpark
1725317250 } else |err| switch (err) {
1725417251 error.Timeout => return,
1725517252 }
1725617253 }
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 }
1726317254};
1726417255
17265/// Spurious wakeups are possible.
17266///
1726717256/// `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 {
1726917258 comptime assert(use_parking_futex or use_parking_sleep);
1727017259 switch (native_os) {
1727117260 .windows => {
1727217261 var timeout_buf: windows.LARGE_INTEGER = undefined;
17273 const raw_timeout: ?*windows.LARGE_INTEGER = if (opt_deadline) |deadline| timeout: {
17274 const now_timestamp = nowWindows(deadline.clock);
17275 const nanoseconds = now_timestamp.durationTo(deadline.raw).nanoseconds;
17276 timeout_buf = @intCast(@divTrunc(-nanoseconds, 100));
17277 break :timeout &timeout_buf;
17278 } else null;
17262 const raw_timeout: ?*windows.LARGE_INTEGER = timeout: switch (timeout) {
17263 .none => null,
17264 .deadline => |timestamp| continue :timeout .{ .duration = .{
17265 .clock = timestamp.clock,
17266 .raw = (nowWindows(timestamp.clock) catch unreachable).durationTo(timestamp.raw),
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 };
1727917274 // `RtlWaitOnAddress` passes the futex address in as the first argument to this call,
1728017275 // but it's unclear what that actually does, especially since `NtAlertThreadByThreadId`
1728117276 // 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
1729717292 },
1729817293 .netbsd => {
1729917294 var ts_buf: posix.timespec = undefined;
17300 const ts: ?*posix.timespec, const clock_real: bool = if (opt_deadline) |deadline| timeout: {
17301 ts_buf = timestampToPosix(deadline.raw.nanoseconds);
17302 break :timeout .{ &ts_buf, deadline.clock == .real };
17303 } else .{ null, true };
17295 const ts: ?*posix.timespec, const abstime: bool, const clock_real: bool = switch (timeout) {
17296 .none => .{ null, false, false },
17297 .deadline => |timestamp| timeout: {
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 };
1730417306 switch (posix.errno(std.c._lwp_park(
1730517307 if (clock_real) .REALTIME else .MONOTONIC,
17306 .{ .ABSTIME = true },
17308 .{ .ABSTIME = abstime },
1730717309 ts,
1730817310 0,
1730917311 addr_hint,