authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-29 08:58:50-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-01 19:17:52-08:00
logc4f5dda135616207c24a6009e221724f5a6fa6aa
tree4ed8421b06679c45a6f1cde327116d3055b4b6d1
parentde87bad4c3240ab34ee81023e6df1ba94d6d5e6a

std.Io.Threaded: re-introduce retry logic behind config


1 files changed, 69 insertions(+), 25 deletions(-)

lib/std/Io/Threaded.zig+69-25
...@@ -53,6 +53,22 @@ cpu_count_error: ?std.Thread.CpuCountError,...@@ -53,6 +53,22 @@ cpu_count_error: ?std.Thread.CpuCountError,
53busy_count: usize = 0,53busy_count: usize = 0,
54main_thread: Thread,54main_thread: Thread,
55pid: Pid = .unknown,55pid: Pid = .unknown,
56/// When a cancel request is made, blocking syscalls can be unblocked by
57/// issuing a signal. However, if the signal arrives after the check and before
58/// the syscall instruction, it is missed.
59///
60/// This option solves the race condition by retrying the signal delivery
61/// until it is acknowledged, with an exponential backoff.
62///
63/// Unfortunately, trying again until the cancellation request is acknowledged
64/// has been observed to be relatively slow, and usually strong cancellation
65/// guarantees are not needed, so this defaults to off.
66///
67/// Musl libc does not have this problem because of a clever, undocumented
68/// extension related to pthread_cancel, which this code integrates with.
69/// When compiling with no libc, `Threaded` does not yet implement the
70/// equivalent trick (tracked by https://codeberg.org/ziglang/zig/issues/30049).
71robust_cancel: RobustCancel = if (is_musl) .enabled else .disabled,
5672
57wsa: if (is_windows) Wsa else struct {} = .{},73wsa: if (is_windows) Wsa else struct {} = .{},
5874
...@@ -60,6 +76,15 @@ have_signal_handler: bool,...@@ -60,6 +76,15 @@ have_signal_handler: bool,
60old_sig_io: if (have_sig_io) posix.Sigaction else void,76old_sig_io: if (have_sig_io) posix.Sigaction else void,
61old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,77old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,
6278
79pub const RobustCancel = if (is_musl) enum {
80 enabled,
81} else if (std.Thread.use_pthreads or native_os == .linux) enum {
82 enabled,
83 disabled,
84} else enum {
85 disabled,
86};
87
63pub const Pid = if (native_os == .linux) enum(posix.pid_t) {88pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
64 unknown = 0,89 unknown = 0,
65 _,90 _,
...@@ -201,7 +226,7 @@ const Closure = struct {...@@ -201,7 +226,7 @@ const Closure = struct {
201 const Start = *const fn (*Closure, *Threaded) void;226 const Start = *const fn (*Closure, *Threaded) void;
202227
203 fn requestCancel(closure: *Closure, t: *Threaded) void {228 fn requestCancel(closure: *Closure, t: *Threaded) void {
204 const signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {229 var signal_id = switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
205 .none, .acknowledged, .requested => return,230 .none, .acknowledged, .requested => return,
206 .signal_id => |signal_id| signal_id,231 .signal_id => |signal_id| signal_id,
207 };232 };
...@@ -214,32 +239,51 @@ const Closure = struct {...@@ -214,32 +239,51 @@ const Closure = struct {
214239
215 // The task will enter a blocking syscall before checking for cancellation again.240 // The task will enter a blocking syscall before checking for cancellation again.
216 // We can send a signal to interrupt the syscall, but if it arrives before241 // We can send a signal to interrupt the syscall, but if it arrives before
217 // the syscall instruction, it will be missed.242 // the syscall instruction, it will be missed. Therefore, this code tries
243 // again until the cancellation request is acknowledged.
244
245 // 1 << 10 ns is about 1 microsecond, approximately syscall overhead.
246 // 1 << 20 ns is about 1 millisecond.
247 // 1 << 30 ns is about 1 second.
218 //248 //
219 // Unfortunately, trying again until the cancellation request is249 // On a heavily loaded Linux 6.17.5, I observed a maximum of 20
220 // acknowledged has been observed to incur a large amount of overhead,250 // attempts not acknowledged before the timeout (including exponential
221 // and usually strong cancellation guarantees are not needed, so the251 // backoff) was sufficient, despite the heavy load.
222 // race condition is not handled here. Users who want to avoid this252 const max_attempts = 22;
223 // have this menu of options instead:253
224 // * Use no libc, in which case Zig std lib can avoid the race (tracking254 for (0..max_attempts) |attempt_index| {
225 // issue: https://codeberg.org/ziglang/zig/issues/30049)255 if (std.Thread.use_pthreads) {
226 // * Use musl libc instead of glibc256 if (std.c.pthread_kill(signal_id, .IO) != 0) return;
227 // * Use `std.Io.Evented`. But this is not implemented yet. Tracked by257 } else if (native_os == .linux) {
228 // - https://codeberg.org/ziglang/zig/issues/30050258 const pid: posix.pid_t = p: {
229 // - https://codeberg.org/ziglang/zig/issues/30051259 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
230 if (std.Thread.use_pthreads) {260 if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
231 if (std.c.pthread_kill(signal_id, .IO) != 0) return;261 const pid = std.os.linux.getpid();
232 } else if (native_os == .linux) {262 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
233 const pid: posix.pid_t = p: {263 break :p pid;
234 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);264 };
235 if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);265 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
236 const pid = std.os.linux.getpid();266 } else {
237 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);267 return;
238 break :p pid;268 }
269
270 if (t.robust_cancel != .enabled) return;
271
272 var timespec: posix.timespec = .{
273 .sec = 0,
274 .nsec = @as(isize, 1) << @intCast(attempt_index),
239 };275 };
240 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;276 if (native_os == .linux) {
241 } else {277 _ = std.os.linux.clock_nanosleep(posix.CLOCK.MONOTONIC, .{ .ABSTIME = false }, &timespec, &timespec);
242 return;278 } else {
279 _ = posix.system.nanosleep(&timespec, &timespec);
280 }
281
282 switch (@atomicRmw(CancelStatus, &closure.cancel_status, .Xchg, .requested, .monotonic).unpack()) {
283 .requested => continue, // Retry needed in case other thread hasn't yet entered the syscall.
284 .none, .acknowledged => return,
285 .signal_id => |new_signal_id| signal_id = new_signal_id,
286 }
243 }287 }
244 }288 }
245};289};