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,
5353busy_count: usize = 0,
5454main_thread: Thread,
5555pid: 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
5773wsa: if (is_windows) Wsa else struct {} = .{},
5874
......@@ -60,6 +76,15 @@ have_signal_handler: bool,
6076old_sig_io: if (have_sig_io) posix.Sigaction else void,
6177old_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
6388pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
6489 unknown = 0,
6590 _,
......@@ -201,7 +226,7 @@ const Closure = struct {
201226 const Start = *const fn (*Closure, *Threaded) void;
202227
203228 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()) {
205230 .none, .acknowledged, .requested => return,
206231 .signal_id => |signal_id| signal_id,
207232 };
......@@ -214,32 +239,51 @@ const Closure = struct {
214239
215240 // The task will enter a blocking syscall before checking for cancellation again.
216241 // 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.
218248 //
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;
249 // On a heavily loaded Linux 6.17.5, I observed a maximum of 20
250 // attempts not acknowledged before the timeout (including exponential
251 // backoff) was sufficient, despite the heavy load.
252 const max_attempts = 22;
253
254 for (0..max_attempts) |attempt_index| {
255 if (std.Thread.use_pthreads) {
256 if (std.c.pthread_kill(signal_id, .IO) != 0) return;
257 } else if (native_os == .linux) {
258 const pid: posix.pid_t = p: {
259 const cached_pid = @atomicLoad(Pid, &t.pid, .monotonic);
260 if (cached_pid != .unknown) break :p @intFromEnum(cached_pid);
261 const pid = std.os.linux.getpid();
262 @atomicStore(Pid, &t.pid, @enumFromInt(pid), .monotonic);
263 break :p pid;
264 };
265 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
266 } else {
267 return;
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),
239275 };
240 if (std.os.linux.tgkill(pid, @bitCast(signal_id), .IO) != 0) return;
241 } else {
242 return;
276 if (native_os == .linux) {
277 _ = std.os.linux.clock_nanosleep(posix.CLOCK.MONOTONIC, .{ .ABSTIME = false }, &timespec, &timespec);
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 }
243287 }
244288 }
245289};