authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-29 08:43:57-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-01 19:17:52-08:00
logde87bad4c3240ab34ee81023e6df1ba94d6d5e6a
treed943199e158ee6c713a8da2d8caa692bc137a94d
parent144206856e26300cc814b3c894527b062db0e69f

std.Io.Threaded: don't solve the cancel race after all

Unfortunately, trying again until the cancellation request is acknowledged has been observed to incur a large amount of overhead, and usually strong cancellation guarantees are not needed, so the race condition is not handled here. Users who want to avoid this have this menu of options instead: * Use no libc, in which case Zig std lib can avoid the race (tracking issue: https://codeberg.org/ziglang/zig/issues/30049) * Use musl libc * Use `std.Io.Evented`. But this is not implemented yet. Tracked by - https://codeberg.org/ziglang/zig/issues/30050 - https://codeberg.org/ziglang/zig/issues/30051 glibc + threaded is the only problematic combination.

1 files changed, 27 insertions(+), 49 deletions(-)

lib/std/Io/Threaded.zig+27-49
......@@ -201,7 +201,7 @@ const Closure = struct {
201201 const Start = *const fn (*Closure, *Threaded) void;
202202
203203 fn requestCancel(closure: *Closure, t: *Threaded) void {
204 var signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
204 const signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
205205 .none, .acknowledged, .requested => return,
206206 .signal_id => |signal_id| signal_id,
207207 };
......@@ -214,54 +214,32 @@ const Closure = struct {
214214
215215 // The task will enter a blocking syscall before checking for cancellation again.
216216 // We can send a signal to interrupt the syscall, but if it arrives before
217 // the syscall instruction, it will be missed. Therefore, this code tries
218 // again until the cancellation request is acknowledged.
219
220 // 1 << 10 ns is about 1 microsecond, approximately syscall overhead.
221 // 1 << 20 ns is about 1 millisecond.
222 // 1 << 30 ns is about 1 second.
223 //
224 // On a heavily loaded Linux 6.17.5, I observed a maximum of 20
225 // attempts not acknowledged before the timeout (including exponential
226 // backoff) was sufficient, despite the heavy load.
217 // the syscall instruction, it will be missed.
227218 //
228 // The time wasted here sleeping is mitigated by the fact that, later
229 // on, the system will likely wait for the canceled task, causing it
230 // to indefinitely yield until the canceled task finishes, and the
231 // task must acknowledge the cancel before it proceeds to that point.
232 const max_attempts = 22;
233
234 for (0..max_attempts) |attempt_index| {
235 if (std.Thread.use_pthreads) {
236 if (std.c.pthread_kill(signal_id, .IO) != 0) return;
237 } else if (native_os == .linux) {
238 const pid: posix.pid_t = p: {
239 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
240 if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
241 const pid = std.os.linux.getpid();
242 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
243 break :p pid;
244 };
245 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
246 } else {
247 return;
248 }
249
250 var timespec: posix.timespec = .{
251 .sec = 0,
252 .nsec = @as(isize, 1) << @intCast(attempt_index),
219 // Unfortunately, trying again until the cancellation request is
220 // acknowledged has been observed to incur a large amount of overhead,
221 // and usually strong cancellation guarantees are not needed, so the
222 // race condition is not handled here. Users who want to avoid this
223 // have this menu of options instead:
224 // * Use no libc, in which case Zig std lib can avoid the race (tracking
225 // issue: https://codeberg.org/ziglang/zig/issues/30049)
226 // * Use musl libc instead of glibc
227 // * Use `std.Io.Evented`. But this is not implemented yet. Tracked by
228 // - https://codeberg.org/ziglang/zig/issues/30050
229 // - https://codeberg.org/ziglang/zig/issues/30051
230 if (std.Thread.use_pthreads) {
231 if (std.c.pthread_kill(signal_id, .IO) != 0) return;
232 } else if (native_os == .linux) {
233 const pid: posix.pid_t = p: {
234 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
235 if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
236 const pid = std.os.linux.getpid();
237 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
238 break :p pid;
253239 };
254 if (native_os == .linux) {
255 _ = std.os.linux.clock_nanosleep(posix.CLOCK.MONOTONIC, .{ .ABSTIME = false }, &timespec, &timespec);
256 } else {
257 _ = posix.system.nanosleep(&timespec, &timespec);
258 }
259
260 switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
261 .requested => continue, // Retry needed in case other thread hasn't yet entered the syscall.
262 .none, .acknowledged => return,
263 .signal_id => |new_signal_id| signal_id = new_signal_id,
264 }
240 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
241 } else {
242 return;
265243 }
266244 }
267245};
......@@ -303,7 +281,7 @@ pub fn init(
303281 .mask = posix.sigemptyset(),
304282 .flags = 0,
305283 };
306 if (have_sig_io) posix.sigaction(.IO, &act, &t.old_sig_io);
284 if (!is_musl and have_sig_io) posix.sigaction(.IO, &act, &t.old_sig_io);
307285 if (have_sig_pipe) posix.sigaction(.PIPE, &act, &t.old_sig_pipe);
308286 t.have_signal_handler = true;
309287 }
......@@ -341,7 +319,7 @@ pub fn deinit(t: *Threaded) void {
341319 if (ws2_32.WSACleanup() != 0) recoverableOsBugDetected();
342320 }
343321 if (posix.Sigaction != void and t.have_signal_handler) {
344 if (have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
322 if (!is_musl and have_sig_io) posix.sigaction(.IO, &t.old_sig_io, null);
345323 if (have_sig_pipe) posix.sigaction(.PIPE, &t.old_sig_pipe, null);
346324 }
347325 t.* = undefined;