| ... | ... | @@ -53,6 +53,22 @@ cpu_count_error: ?std.Thread.CpuCountError, |
| 53 | 53 | busy_count: usize = 0, |
| 54 | 54 | main_thread: Thread, |
| 55 | 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 | 73 | wsa: if (is_windows) Wsa else struct {} = .{}, |
| 58 | 74 | |
| ... | ... | @@ -60,6 +76,15 @@ have_signal_handler: bool, |
| 60 | 76 | old_sig_io: if (have_sig_io) posix.Sigaction else void, |
| 61 | 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 | 88 | pub const Pid = if (native_os == .linux) enum(posix.pid_t) { |
| 64 | 89 | unknown = 0, |
| 65 | 90 | _, |
| ... | ... | @@ -201,7 +226,7 @@ const Closure = struct { |
| 201 | 226 | const Start = *const fn (*Closure, *Threaded) void; |
| 202 | 227 | |
| 203 | 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 | 230 | .none, .acknowledged, .requested => return, |
| 206 | 231 | .signal_id => |signal_id| signal_id, |
| 207 | 232 | }; |
| ... | ... | @@ -214,32 +239,51 @@ const Closure = struct { |
| 214 | 239 | |
| 215 | 240 | // The task will enter a blocking syscall before checking for cancellation again. |
| 216 | 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 |
| 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), |
| 239 | 275 | }; |
| 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 | } |
| 243 | 287 | } |
| 244 | 288 | } |
| 245 | 289 | }; |