| ... | @@ -53,6 +53,22 @@ cpu_count_error: ?std.Thread.CpuCountError, | ... | @@ -53,6 +53,22 @@ cpu_count_error: ?std.Thread.CpuCountError, |
| 53 | busy_count: usize = 0, | 53 | busy_count: usize = 0, |
| 54 | main_thread: Thread, | 54 | main_thread: Thread, |
| 55 | pid: Pid = .unknown, | 55 | pid: 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). |
| | 71 | robust_cancel: RobustCancel = if (is_musl) .enabled else .disabled, |
| 56 | | 72 | |
| 57 | wsa: if (is_windows) Wsa else struct {} = .{}, | 73 | wsa: if (is_windows) Wsa else struct {} = .{}, |
| 58 | | 74 | |
| ... | @@ -60,6 +76,15 @@ have_signal_handler: bool, | ... | @@ -60,6 +76,15 @@ have_signal_handler: bool, |
| 60 | old_sig_io: if (have_sig_io) posix.Sigaction else void, | 76 | old_sig_io: if (have_sig_io) posix.Sigaction else void, |
| 61 | old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void, | 77 | old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void, |
| 62 | | 78 | |
| | 79 | pub 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 | |
| 63 | pub const Pid = if (native_os == .linux) enum(posix.pid_t) { | 88 | pub 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; |
| 202 | | 227 | |
| 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 { |
| 214 | | 239 | |
| 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 before | 241 | // 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 is | 249 | // 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 the | 251 | // backoff) was sufficient, despite the heavy load. |
| 222 | // race condition is not handled here. Users who want to avoid this | 252 | 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 (tracking | 254 | 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 glibc | 256 | if (std.c.pthread_kill(signal_id, .IO) != 0) return; |
| 227 | // * Use `std.Io.Evented`. But this is not implemented yet. Tracked by | 257 | } else if (native_os == .linux) { |
| 228 | // - https://codeberg.org/ziglang/zig/issues/30050 | 258 | const pid: posix.pid_t = p: { |
| 229 | // - https://codeberg.org/ziglang/zig/issues/30051 | 259 | 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 | }; |