| author | |
| committer | |
| log | a7790bd32e1e8caf6f2f0bedede8a7cb7b35c443 |
| tree | 968158158976a01daf6dccf9bc002ecd60906e5e |
| parent | 012ef81b8ba63f757366384a7721647481665762 |
3 files changed, 496 insertions(+), 27 deletions(-)
lib/std/Io.zig+310-12| ... | @@ -1,7 +1,5 @@ | ... | @@ -1,7 +1,5 @@ |
| 1 | const std = @import("std.zig"); | ||
| 2 | const builtin = @import("builtin"); | 1 | const builtin = @import("builtin"); |
| 3 | const root = @import("root"); | 2 | const std = @import("std.zig"); |
| 4 | const c = std.c; | ||
| 5 | const is_windows = builtin.os.tag == .windows; | 3 | const is_windows = builtin.os.tag == .windows; |
| 6 | const windows = std.os.windows; | 4 | const windows = std.os.windows; |
| 7 | const posix = std.posix; | 5 | const posix = std.posix; |
| ... | @@ -9,8 +7,6 @@ const math = std.math; | ... | @@ -9,8 +7,6 @@ const math = std.math; |
| 9 | const assert = std.debug.assert; | 7 | const assert = std.debug.assert; |
| 10 | const fs = std.fs; | 8 | const fs = std.fs; |
| 11 | const mem = std.mem; | 9 | const mem = std.mem; |
| 12 | const meta = std.meta; | ||
| 13 | const File = std.fs.File; | ||
| 14 | const Allocator = std.mem.Allocator; | 10 | const Allocator = std.mem.Allocator; |
| 15 | const Alignment = std.mem.Alignment; | 11 | const Alignment = std.mem.Alignment; |
| 16 | 12 | ||
| ... | @@ -972,6 +968,12 @@ pub const VTable = struct { | ... | @@ -972,6 +968,12 @@ pub const VTable = struct { |
| 972 | /// Thread-safe. | 968 | /// Thread-safe. |
| 973 | cancelRequested: *const fn (?*anyopaque) bool, | 969 | cancelRequested: *const fn (?*anyopaque) bool, |
| 974 | 970 | ||
| 971 | mutexLock: *const fn (?*anyopaque, mutex: *Mutex) void, | ||
| 972 | mutexUnlock: *const fn (?*anyopaque, mutex: *Mutex) void, | ||
| 973 | |||
| 974 | conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex, timeout_ns: ?u64) Condition.WaitError!void, | ||
| 975 | conditionWake: *const fn (?*anyopaque, cond: *Condition, notify: Condition.Notify) void, | ||
| 976 | |||
| 975 | createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, | 977 | createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, |
| 976 | openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, | 978 | openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, |
| 977 | closeFile: *const fn (?*anyopaque, fs.File) void, | 979 | closeFile: *const fn (?*anyopaque, fs.File) void, |
| ... | @@ -985,11 +987,11 @@ pub const VTable = struct { | ... | @@ -985,11 +987,11 @@ pub const VTable = struct { |
| 985 | pub const OpenFlags = fs.File.OpenFlags; | 987 | pub const OpenFlags = fs.File.OpenFlags; |
| 986 | pub const CreateFlags = fs.File.CreateFlags; | 988 | pub const CreateFlags = fs.File.CreateFlags; |
| 987 | 989 | ||
| 988 | pub const FileOpenError = fs.File.OpenError || error{AsyncCancel}; | 990 | pub const FileOpenError = fs.File.OpenError || error{Canceled}; |
| 989 | pub const FileReadError = fs.File.ReadError || error{AsyncCancel}; | 991 | pub const FileReadError = fs.File.ReadError || error{Canceled}; |
| 990 | pub const FilePReadError = fs.File.PReadError || error{AsyncCancel}; | 992 | pub const FilePReadError = fs.File.PReadError || error{Canceled}; |
| 991 | pub const FileWriteError = fs.File.WriteError || error{AsyncCancel}; | 993 | pub const FileWriteError = fs.File.WriteError || error{Canceled}; |
| 992 | pub const FilePWriteError = fs.File.PWriteError || error{AsyncCancel}; | 994 | pub const FilePWriteError = fs.File.PWriteError || error{Canceled}; |
| 993 | 995 | ||
| 994 | pub const Timestamp = enum(i96) { | 996 | pub const Timestamp = enum(i96) { |
| 995 | _, | 997 | _, |
| ... | @@ -1006,8 +1008,8 @@ pub const Deadline = union(enum) { | ... | @@ -1006,8 +1008,8 @@ pub const Deadline = union(enum) { |
| 1006 | nanoseconds: i96, | 1008 | nanoseconds: i96, |
| 1007 | timestamp: Timestamp, | 1009 | timestamp: Timestamp, |
| 1008 | }; | 1010 | }; |
| 1009 | pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{AsyncCancel}; | 1011 | pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{Canceled}; |
| 1010 | pub const SleepError = error{ UnsupportedClock, Unexpected, AsyncCancel }; | 1012 | pub const SleepError = error{ UnsupportedClock, Unexpected, Canceled }; |
| 1011 | 1013 | ||
| 1012 | pub const AnyFuture = opaque {}; | 1014 | pub const AnyFuture = opaque {}; |
| 1013 | 1015 | ||
| ... | @@ -1036,6 +1038,302 @@ pub fn Future(Result: type) type { | ... | @@ -1036,6 +1038,302 @@ pub fn Future(Result: type) type { |
| 1036 | }; | 1038 | }; |
| 1037 | } | 1039 | } |
| 1038 | 1040 | ||
| 1041 | pub const Mutex = struct { | ||
| 1042 | state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unlocked), | ||
| 1043 | |||
| 1044 | pub const unlocked: u32 = 0b00; | ||
| 1045 | pub const locked: u32 = 0b01; | ||
| 1046 | pub const contended: u32 = 0b11; // must contain the `locked` bit for x86 optimization below | ||
| 1047 | |||
| 1048 | pub fn tryLock(m: *Mutex) bool { | ||
| 1049 | // On x86, use `lock bts` instead of `lock cmpxchg` as: | ||
| 1050 | // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048 | ||
| 1051 | // - `lock bts` is smaller instruction-wise which makes it better for inlining | ||
| 1052 | if (builtin.target.cpu.arch.isX86()) { | ||
| 1053 | const locked_bit = @ctz(locked); | ||
| 1054 | return m.state.bitSet(locked_bit, .acquire) == 0; | ||
| 1055 | } | ||
| 1056 | |||
| 1057 | // Acquire barrier ensures grabbing the lock happens before the critical section | ||
| 1058 | // and that the previous lock holder's critical section happens before we grab the lock. | ||
| 1059 | return m.state.cmpxchgWeak(unlocked, locked, .acquire, .monotonic) == null; | ||
| 1060 | } | ||
| 1061 | |||
| 1062 | /// Avoids the vtable for uncontended locks. | ||
| 1063 | pub fn lock(m: *Mutex, io: Io) void { | ||
| 1064 | if (!m.tryLock()) { | ||
| 1065 | @branchHint(.unlikely); | ||
| 1066 | io.vtable.mutexLock(io.userdata, m); | ||
| 1067 | } | ||
| 1068 | } | ||
| 1069 | |||
| 1070 | pub fn unlock(m: *Mutex, io: Io) void { | ||
| 1071 | io.vtable.mutexUnlock(io.userdata, m); | ||
| 1072 | } | ||
| 1073 | }; | ||
| 1074 | |||
| 1075 | pub const Condition = struct { | ||
| 1076 | state: u64 = 0, | ||
| 1077 | |||
| 1078 | pub const WaitError = error{ | ||
| 1079 | Timeout, | ||
| 1080 | Canceled, | ||
| 1081 | }; | ||
| 1082 | |||
| 1083 | /// How many waiters to wake up. | ||
| 1084 | pub const Notify = enum { | ||
| 1085 | one, | ||
| 1086 | all, | ||
| 1087 | }; | ||
| 1088 | |||
| 1089 | pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) void { | ||
| 1090 | io.vtable.conditionWait(io.userdata, cond, mutex, null) catch |err| switch (err) { | ||
| 1091 | error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out | ||
| 1092 | error.Canceled => return, // handled as spurious wakeup | ||
| 1093 | }; | ||
| 1094 | } | ||
| 1095 | |||
| 1096 | pub fn timedWait(cond: *Condition, io: Io, mutex: *Mutex, timeout_ns: u64) WaitError!void { | ||
| 1097 | return io.vtable.conditionWait(io.userdata, cond, mutex, timeout_ns); | ||
| 1098 | } | ||
| 1099 | |||
| 1100 | pub fn signal(cond: *Condition, io: Io) void { | ||
| 1101 | io.vtable.conditionWake(io.userdata, cond, .one); | ||
| 1102 | } | ||
| 1103 | |||
| 1104 | pub fn broadcast(cond: *Condition, io: Io) void { | ||
| 1105 | io.vtable.conditionWake(io.userdata, cond, .all); | ||
| 1106 | } | ||
| 1107 | }; | ||
| 1108 | |||
| 1109 | pub const TypeErasedQueue = struct { | ||
| 1110 | mutex: Mutex, | ||
| 1111 | |||
| 1112 | /// Ring buffer. This data is logically *after* queued getters. | ||
| 1113 | buffer: []u8, | ||
| 1114 | put_index: usize, | ||
| 1115 | get_index: usize, | ||
| 1116 | |||
| 1117 | putters: std.DoublyLinkedList(PutNode), | ||
| 1118 | getters: std.DoublyLinkedList(GetNode), | ||
| 1119 | |||
| 1120 | const PutNode = struct { | ||
| 1121 | remaining: []const u8, | ||
| 1122 | condition: Condition, | ||
| 1123 | }; | ||
| 1124 | |||
| 1125 | const GetNode = struct { | ||
| 1126 | remaining: []u8, | ||
| 1127 | condition: Condition, | ||
| 1128 | }; | ||
| 1129 | |||
| 1130 | pub fn init(buffer: []u8) TypeErasedQueue { | ||
| 1131 | return .{ | ||
| 1132 | .mutex = .{}, | ||
| 1133 | .buffer = buffer, | ||
| 1134 | .put_index = 0, | ||
| 1135 | .get_index = 0, | ||
| 1136 | .putters = .{}, | ||
| 1137 | .getters = .{}, | ||
| 1138 | }; | ||
| 1139 | } | ||
| 1140 | |||
| 1141 | pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize { | ||
| 1142 | assert(elements.len >= min); | ||
| 1143 | |||
| 1144 | q.mutex.lock(io); | ||
| 1145 | defer q.mutex.unlock(io); | ||
| 1146 | |||
| 1147 | // Getters have first priority on the data, and only when the getters | ||
| 1148 | // queue is empty do we start populating the buffer. | ||
| 1149 | |||
| 1150 | var remaining = elements; | ||
| 1151 | while (true) { | ||
| 1152 | const getter = q.getters.popFirst() orelse break; | ||
| 1153 | const copy_len = @min(getter.data.remaining.len, remaining.len); | ||
| 1154 | @memcpy(getter.data.remaining[0..copy_len], remaining[0..copy_len]); | ||
| 1155 | remaining = remaining[copy_len..]; | ||
| 1156 | getter.data.remaining = getter.data.remaining[copy_len..]; | ||
| 1157 | if (getter.data.remaining.len == 0) { | ||
| 1158 | getter.data.condition.signal(io); | ||
| 1159 | continue; | ||
| 1160 | } | ||
| 1161 | q.getters.prepend(getter); | ||
| 1162 | assert(remaining.len == 0); | ||
| 1163 | return elements.len; | ||
| 1164 | } | ||
| 1165 | |||
| 1166 | while (true) { | ||
| 1167 | { | ||
| 1168 | const available = q.buffer[q.put_index..]; | ||
| 1169 | const copy_len = @min(available.len, remaining.len); | ||
| 1170 | @memcpy(available[0..copy_len], remaining[0..copy_len]); | ||
| 1171 | remaining = remaining[copy_len..]; | ||
| 1172 | q.put_index += copy_len; | ||
| 1173 | if (remaining.len == 0) return elements.len; | ||
| 1174 | } | ||
| 1175 | { | ||
| 1176 | const available = q.buffer[0..q.get_index]; | ||
| 1177 | const copy_len = @min(available.len, remaining.len); | ||
| 1178 | @memcpy(available[0..copy_len], remaining[0..copy_len]); | ||
| 1179 | remaining = remaining[copy_len..]; | ||
| 1180 | q.put_index = copy_len; | ||
| 1181 | if (remaining.len == 0) return elements.len; | ||
| 1182 | } | ||
| 1183 | |||
| 1184 | const total_filled = elements.len - remaining.len; | ||
| 1185 | if (total_filled >= min) return total_filled; | ||
| 1186 | |||
| 1187 | var node: std.DoublyLinkedList(PutNode).Node = .{ | ||
| 1188 | .data = .{ .remaining = remaining, .condition = .{} }, | ||
| 1189 | }; | ||
| 1190 | q.putters.append(&node); | ||
| 1191 | node.data.condition.wait(io, &q.mutex); | ||
| 1192 | remaining = node.data.remaining; | ||
| 1193 | } | ||
| 1194 | } | ||
| 1195 | |||
| 1196 | pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) usize { | ||
| 1197 | assert(buffer.len >= min); | ||
| 1198 | |||
| 1199 | q.mutex.lock(io); | ||
| 1200 | defer q.mutex.unlock(io); | ||
| 1201 | |||
| 1202 | // The ring buffer gets first priority, then data should come from any | ||
| 1203 | // queued putters, then finally the ring buffer should be filled with | ||
| 1204 | // data from putters so they can be resumed. | ||
| 1205 | |||
| 1206 | var remaining = buffer; | ||
| 1207 | while (true) { | ||
| 1208 | if (q.get_index <= q.put_index) { | ||
| 1209 | const available = q.buffer[q.get_index..q.put_index]; | ||
| 1210 | const copy_len = @min(available.len, remaining.len); | ||
| 1211 | @memcpy(remaining[0..copy_len], available[0..copy_len]); | ||
| 1212 | q.get_index += copy_len; | ||
| 1213 | remaining = remaining[copy_len..]; | ||
| 1214 | if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 1215 | } else { | ||
| 1216 | { | ||
| 1217 | const available = q.buffer[q.get_index..]; | ||
| 1218 | const copy_len = @min(available.len, remaining.len); | ||
| 1219 | @memcpy(remaining[0..copy_len], available[0..copy_len]); | ||
| 1220 | q.get_index += copy_len; | ||
| 1221 | remaining = remaining[copy_len..]; | ||
| 1222 | if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 1223 | } | ||
| 1224 | { | ||
| 1225 | const available = q.buffer[0..q.put_index]; | ||
| 1226 | const copy_len = @min(available.len, remaining.len); | ||
| 1227 | @memcpy(remaining[0..copy_len], available[0..copy_len]); | ||
| 1228 | q.get_index = copy_len; | ||
| 1229 | remaining = remaining[copy_len..]; | ||
| 1230 | if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 1231 | } | ||
| 1232 | } | ||
| 1233 | // Copy directly from putters into buffer. | ||
| 1234 | while (remaining.len > 0) { | ||
| 1235 | const putter = q.putters.popFirst() orelse break; | ||
| 1236 | const copy_len = @min(putter.data.remaining.len, remaining.len); | ||
| 1237 | @memcpy(remaining[0..copy_len], putter.data.remaining[0..copy_len]); | ||
| 1238 | putter.data.remaining = putter.data.remaining[copy_len..]; | ||
| 1239 | remaining = remaining[copy_len..]; | ||
| 1240 | if (putter.data.remaining.len == 0) { | ||
| 1241 | putter.data.condition.signal(io); | ||
| 1242 | } else { | ||
| 1243 | assert(remaining.len == 0); | ||
| 1244 | q.putters.prepend(putter); | ||
| 1245 | return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 1246 | } | ||
| 1247 | } | ||
| 1248 | // Both ring buffer and putters queue is empty. | ||
| 1249 | const total_filled = buffer.len - remaining.len; | ||
| 1250 | if (total_filled >= min) return total_filled; | ||
| 1251 | |||
| 1252 | var node: std.DoublyLinkedList(GetNode).Node = .{ | ||
| 1253 | .data = .{ .remaining = remaining, .condition = .{} }, | ||
| 1254 | }; | ||
| 1255 | q.getters.append(&node); | ||
| 1256 | node.data.condition.wait(io, &q.mutex); | ||
| 1257 | remaining = node.data.remaining; | ||
| 1258 | } | ||
| 1259 | } | ||
| 1260 | |||
| 1261 | /// Called when there is nonzero space available in the ring buffer and | ||
| 1262 | /// potentially putters waiting. The mutex is already held and the task is | ||
| 1263 | /// to copy putter data to the ring buffer and signal any putters whose | ||
| 1264 | /// buffers been fully copied. | ||
| 1265 | fn fillRingBufferFromPutters(q: *TypeErasedQueue, io: Io, len: usize) usize { | ||
| 1266 | while (true) { | ||
| 1267 | const putter = q.putters.popFirst() orelse return len; | ||
| 1268 | const available = q.buffer[q.put_index..]; | ||
| 1269 | const copy_len = @min(available.len, putter.data.remaining.len); | ||
| 1270 | @memcpy(available[0..copy_len], putter.data.remaining[0..copy_len]); | ||
| 1271 | putter.data.remaining = putter.data.remaining[copy_len..]; | ||
| 1272 | q.put_index += copy_len; | ||
| 1273 | if (putter.data.remaining.len == 0) { | ||
| 1274 | putter.data.condition.signal(io); | ||
| 1275 | continue; | ||
| 1276 | } | ||
| 1277 | const second_available = q.buffer[0..q.get_index]; | ||
| 1278 | const second_copy_len = @min(second_available.len, putter.data.remaining.len); | ||
| 1279 | @memcpy(second_available[0..second_copy_len], putter.data.remaining[0..second_copy_len]); | ||
| 1280 | putter.data.remaining = putter.data.remaining[copy_len..]; | ||
| 1281 | q.put_index = copy_len; | ||
| 1282 | if (putter.data.remaining.len == 0) { | ||
| 1283 | putter.data.condition.signal(io); | ||
| 1284 | continue; | ||
| 1285 | } | ||
| 1286 | q.putters.prepend(putter); | ||
| 1287 | return len; | ||
| 1288 | } | ||
| 1289 | } | ||
| 1290 | }; | ||
| 1291 | |||
| 1292 | /// Many producer, many consumer, thread-safe, runtime configurable buffer size. | ||
| 1293 | /// When buffer is empty, consumers suspend and are resumed by producers. | ||
| 1294 | /// When buffer is full, producers suspend and are resumed by consumers. | ||
| 1295 | pub fn Queue(Elem: type) type { | ||
| 1296 | return struct { | ||
| 1297 | type_erased: TypeErasedQueue, | ||
| 1298 | |||
| 1299 | pub fn init(buffer: []Elem) @This() { | ||
| 1300 | return .{ .type_erased = .init(@ptrCast(buffer)) }; | ||
| 1301 | } | ||
| 1302 | |||
| 1303 | /// Appends elements to the end of the queue. The function returns when | ||
| 1304 | /// at least `min` elements have been added to the buffer or sent | ||
| 1305 | /// directly to a consumer. | ||
| 1306 | /// | ||
| 1307 | /// Returns how many elements have been added to the queue. | ||
| 1308 | /// | ||
| 1309 | /// Asserts that `elements.len >= min`. | ||
| 1310 | pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) usize { | ||
| 1311 | return @divExact(q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); | ||
| 1312 | } | ||
| 1313 | |||
| 1314 | /// Receives elements from the beginning of the queue. The function | ||
| 1315 | /// returns when at least `min` elements have been populated inside | ||
| 1316 | /// `buffer`. | ||
| 1317 | /// | ||
| 1318 | /// Returns how many elements of `buffer` have been populated. | ||
| 1319 | /// | ||
| 1320 | /// Asserts that `buffer.len >= min`. | ||
| 1321 | pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) usize { | ||
| 1322 | return @divExact(q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); | ||
| 1323 | } | ||
| 1324 | |||
| 1325 | pub fn putOne(q: *@This(), io: Io, item: Elem) void { | ||
| 1326 | assert(q.put(io, &.{item}, 1) == 1); | ||
| 1327 | } | ||
| 1328 | |||
| 1329 | pub fn getOne(q: *@This(), io: Io) Elem { | ||
| 1330 | var buf: [1]Elem = undefined; | ||
| 1331 | assert(q.get(io, &buf, 1) == 1); | ||
| 1332 | return buf[0]; | ||
| 1333 | } | ||
| 1334 | }; | ||
| 1335 | } | ||
| 1336 | |||
| 1039 | /// Calls `function` with `args`, such that the return value of the function is | 1337 | /// Calls `function` with `args`, such that the return value of the function is |
| 1040 | /// not guaranteed to be available until `await` is called. | 1338 | /// not guaranteed to be available until `await` is called. |
| 1041 | pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { | 1339 | pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { |
lib/std/Io/EventLoop.zig+7-7| ... | @@ -102,7 +102,7 @@ const Fiber = struct { | ... | @@ -102,7 +102,7 @@ const Fiber = struct { |
| 102 | return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); | 102 | return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{AsyncCancel}!void { | 105 | fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{Canceled}!void { |
| 106 | if (@cmpxchgStrong( | 106 | if (@cmpxchgStrong( |
| 107 | ?*Thread, | 107 | ?*Thread, |
| 108 | &fiber.cancel_thread, | 108 | &fiber.cancel_thread, |
| ... | @@ -112,7 +112,7 @@ const Fiber = struct { | ... | @@ -112,7 +112,7 @@ const Fiber = struct { |
| 112 | .acquire, | 112 | .acquire, |
| 113 | )) |cancel_thread| { | 113 | )) |cancel_thread| { |
| 114 | assert(cancel_thread == Thread.canceling); | 114 | assert(cancel_thread == Thread.canceling); |
| 115 | return error.AsyncCancel; | 115 | return error.Canceled; |
| 116 | } | 116 | } |
| 117 | } | 117 | } |
| 118 | 118 | ||
| ... | @@ -746,7 +746,7 @@ pub fn createFile( | ... | @@ -746,7 +746,7 @@ pub fn createFile( |
| 746 | switch (errno(completion.result)) { | 746 | switch (errno(completion.result)) { |
| 747 | .SUCCESS => return .{ .handle = completion.result }, | 747 | .SUCCESS => return .{ .handle = completion.result }, |
| 748 | .INTR => unreachable, | 748 | .INTR => unreachable, |
| 749 | .CANCELED => return error.AsyncCancel, | 749 | .CANCELED => return error.Canceled, |
| 750 | 750 | ||
| 751 | .FAULT => unreachable, | 751 | .FAULT => unreachable, |
| 752 | .INVAL => return error.BadPathName, | 752 | .INVAL => return error.BadPathName, |
| ... | @@ -854,7 +854,7 @@ pub fn openFile( | ... | @@ -854,7 +854,7 @@ pub fn openFile( |
| 854 | switch (errno(completion.result)) { | 854 | switch (errno(completion.result)) { |
| 855 | .SUCCESS => return .{ .handle = completion.result }, | 855 | .SUCCESS => return .{ .handle = completion.result }, |
| 856 | .INTR => unreachable, | 856 | .INTR => unreachable, |
| 857 | .CANCELED => return error.AsyncCancel, | 857 | .CANCELED => return error.Canceled, |
| 858 | 858 | ||
| 859 | .FAULT => unreachable, | 859 | .FAULT => unreachable, |
| 860 | .INVAL => return error.BadPathName, | 860 | .INVAL => return error.BadPathName, |
| ... | @@ -950,7 +950,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std | ... | @@ -950,7 +950,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std |
| 950 | switch (errno(completion.result)) { | 950 | switch (errno(completion.result)) { |
| 951 | .SUCCESS => return @as(u32, @bitCast(completion.result)), | 951 | .SUCCESS => return @as(u32, @bitCast(completion.result)), |
| 952 | .INTR => unreachable, | 952 | .INTR => unreachable, |
| 953 | .CANCELED => return error.AsyncCancel, | 953 | .CANCELED => return error.Canceled, |
| 954 | 954 | ||
| 955 | .INVAL => unreachable, | 955 | .INVAL => unreachable, |
| 956 | .FAULT => unreachable, | 956 | .FAULT => unreachable, |
| ... | @@ -1002,7 +1002,7 @@ pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offs | ... | @@ -1002,7 +1002,7 @@ pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offs |
| 1002 | switch (errno(completion.result)) { | 1002 | switch (errno(completion.result)) { |
| 1003 | .SUCCESS => return @as(u32, @bitCast(completion.result)), | 1003 | .SUCCESS => return @as(u32, @bitCast(completion.result)), |
| 1004 | .INTR => unreachable, | 1004 | .INTR => unreachable, |
| 1005 | .CANCELED => return error.AsyncCancel, | 1005 | .CANCELED => return error.Canceled, |
| 1006 | 1006 | ||
| 1007 | .INVAL => return error.InvalidArgument, | 1007 | .INVAL => return error.InvalidArgument, |
| 1008 | .FAULT => unreachable, | 1008 | .FAULT => unreachable, |
| ... | @@ -1080,7 +1080,7 @@ pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.D | ... | @@ -1080,7 +1080,7 @@ pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.D |
| 1080 | switch (errno(completion.result)) { | 1080 | switch (errno(completion.result)) { |
| 1081 | .SUCCESS, .TIME => return, | 1081 | .SUCCESS, .TIME => return, |
| 1082 | .INTR => unreachable, | 1082 | .INTR => unreachable, |
| 1083 | .CANCELED => return error.AsyncCancel, | 1083 | .CANCELED => return error.Canceled, |
| 1084 | 1084 | ||
| 1085 | else => |err| return std.posix.unexpectedErrno(err), | 1085 | else => |err| return std.posix.unexpectedErrno(err), |
| 1086 | } | 1086 | } |
lib/std/Thread/Pool.zig+179-8| ... | @@ -332,9 +332,12 @@ pub fn io(pool: *Pool) Io { | ... | @@ -332,9 +332,12 @@ pub fn io(pool: *Pool) Io { |
| 332 | .vtable = &.{ | 332 | .vtable = &.{ |
| 333 | .@"async" = @"async", | 333 | .@"async" = @"async", |
| 334 | .@"await" = @"await", | 334 | .@"await" = @"await", |
| 335 | |||
| 336 | .cancel = cancel, | 335 | .cancel = cancel, |
| 337 | .cancelRequested = cancelRequested, | 336 | .cancelRequested = cancelRequested, |
| 337 | .mutexLock = mutexLock, | ||
| 338 | .mutexUnlock = mutexUnlock, | ||
| 339 | .conditionWait = conditionWait, | ||
| 340 | .conditionWake = conditionWake, | ||
| 338 | 341 | ||
| 339 | .createFile = createFile, | 342 | .createFile = createFile, |
| 340 | .openFile = openFile, | 343 | .openFile = openFile, |
| ... | @@ -517,11 +520,179 @@ fn cancelRequested(userdata: ?*anyopaque) bool { | ... | @@ -517,11 +520,179 @@ fn cancelRequested(userdata: ?*anyopaque) bool { |
| 517 | return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; | 520 | return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; |
| 518 | } | 521 | } |
| 519 | 522 | ||
| 520 | fn checkCancel(pool: *Pool) error{AsyncCancel}!void { | 523 | fn checkCancel(pool: *Pool) error{Canceled}!void { |
| 521 | if (cancelRequested(pool)) return error.AsyncCancel; | 524 | if (cancelRequested(pool)) return error.Canceled; |
| 525 | } | ||
| 526 | |||
| 527 | fn mutexLock(userdata: ?*anyopaque, m: *Io.Mutex) void { | ||
| 528 | @branchHint(.cold); | ||
| 529 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 530 | _ = pool; | ||
| 531 | |||
| 532 | // Avoid doing an atomic swap below if we already know the state is contended. | ||
| 533 | // An atomic swap unconditionally stores which marks the cache-line as modified unnecessarily. | ||
| 534 | if (m.state.load(.monotonic) == Io.Mutex.contended) { | ||
| 535 | std.Thread.Futex.wait(&m.state, Io.Mutex.contended); | ||
| 536 | } | ||
| 537 | |||
| 538 | // Try to acquire the lock while also telling the existing lock holder that there are threads waiting. | ||
| 539 | // | ||
| 540 | // Once we sleep on the Futex, we must acquire the mutex using `contended` rather than `locked`. | ||
| 541 | // If not, threads sleeping on the Futex wouldn't see the state change in unlock and potentially deadlock. | ||
| 542 | // The downside is that the last mutex unlocker will see `contended` and do an unnecessary Futex wake | ||
| 543 | // but this is better than having to wake all waiting threads on mutex unlock. | ||
| 544 | // | ||
| 545 | // Acquire barrier ensures grabbing the lock happens before the critical section | ||
| 546 | // and that the previous lock holder's critical section happens before we grab the lock. | ||
| 547 | while (m.state.swap(Io.Mutex.contended, .acquire) != Io.Mutex.unlocked) { | ||
| 548 | std.Thread.Futex.wait(&m.state, Io.Mutex.contended); | ||
| 549 | } | ||
| 550 | } | ||
| 551 | |||
| 552 | fn mutexUnlock(userdata: ?*anyopaque, m: *Io.Mutex) void { | ||
| 553 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 554 | _ = pool; | ||
| 555 | // Needs to also wake up a waiting thread if any. | ||
| 556 | // | ||
| 557 | // A waiting thread will acquire with `contended` instead of `locked` | ||
| 558 | // which ensures that it wakes up another thread on the next unlock(). | ||
| 559 | // | ||
| 560 | // Release barrier ensures the critical section happens before we let go of the lock | ||
| 561 | // and that our critical section happens before the next lock holder grabs the lock. | ||
| 562 | const state = m.state.swap(Io.Mutex.unlocked, .release); | ||
| 563 | assert(state != Io.Mutex.unlocked); | ||
| 564 | |||
| 565 | if (state == Io.Mutex.contended) { | ||
| 566 | std.Thread.Futex.wake(&m.state, 1); | ||
| 567 | } | ||
| 568 | } | ||
| 569 | |||
| 570 | fn mutexLockInternal(pool: *std.Thread.Pool, m: *Io.Mutex) void { | ||
| 571 | if (!m.tryLock()) { | ||
| 572 | @branchHint(.unlikely); | ||
| 573 | mutexLock(pool, m); | ||
| 574 | } | ||
| 575 | } | ||
| 576 | |||
| 577 | fn conditionWait( | ||
| 578 | userdata: ?*anyopaque, | ||
| 579 | cond: *Io.Condition, | ||
| 580 | mutex: *Io.Mutex, | ||
| 581 | timeout: ?u64, | ||
| 582 | ) Io.Condition.WaitError!void { | ||
| 583 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 584 | comptime assert(@TypeOf(cond.state) == u64); | ||
| 585 | const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); | ||
| 586 | const cond_state = &ints[0]; | ||
| 587 | const cond_epoch = &ints[1]; | ||
| 588 | const one_waiter = 1; | ||
| 589 | const waiter_mask = 0xffff; | ||
| 590 | const one_signal = 1 << 16; | ||
| 591 | const signal_mask = 0xffff << 16; | ||
| 592 | // Observe the epoch, then check the state again to see if we should wake up. | ||
| 593 | // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock: | ||
| 594 | // | ||
| 595 | // - T1: s = LOAD(&state) | ||
| 596 | // - T2: UPDATE(&s, signal) | ||
| 597 | // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch) | ||
| 598 | // - T1: e = LOAD(&epoch) (was reordered after the state load) | ||
| 599 | // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change) | ||
| 600 | // | ||
| 601 | // Acquire barrier to ensure the epoch load happens before the state load. | ||
| 602 | var epoch = cond_epoch.load(.acquire); | ||
| 603 | var state = cond_state.fetchAdd(one_waiter, .monotonic); | ||
| 604 | assert(state & waiter_mask != waiter_mask); | ||
| 605 | state += one_waiter; | ||
| 606 | |||
| 607 | mutexUnlock(pool, mutex); | ||
| 608 | defer mutexLockInternal(pool, mutex); | ||
| 609 | |||
| 610 | var futex_deadline = std.Thread.Futex.Deadline.init(timeout); | ||
| 611 | |||
| 612 | while (true) { | ||
| 613 | futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { | ||
| 614 | // On timeout, we must decrement the waiter we added above. | ||
| 615 | error.Timeout => { | ||
| 616 | while (true) { | ||
| 617 | // If there's a signal when we're timing out, consume it and report being woken up instead. | ||
| 618 | // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. | ||
| 619 | while (state & signal_mask != 0) { | ||
| 620 | const new_state = state - one_waiter - one_signal; | ||
| 621 | state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; | ||
| 622 | } | ||
| 623 | |||
| 624 | // Remove the waiter we added and officially return timed out. | ||
| 625 | const new_state = state - one_waiter; | ||
| 626 | state = cond_state.cmpxchgWeak(state, new_state, .monotonic, .monotonic) orelse return err; | ||
| 627 | } | ||
| 628 | }, | ||
| 629 | }; | ||
| 630 | |||
| 631 | epoch = cond_epoch.load(.acquire); | ||
| 632 | state = cond_state.load(.monotonic); | ||
| 633 | |||
| 634 | // Try to wake up by consuming a signal and decremented the waiter we added previously. | ||
| 635 | // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. | ||
| 636 | while (state & signal_mask != 0) { | ||
| 637 | const new_state = state - one_waiter - one_signal; | ||
| 638 | state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; | ||
| 639 | } | ||
| 640 | } | ||
| 641 | } | ||
| 642 | |||
| 643 | fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Condition.Notify) void { | ||
| 644 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 645 | _ = pool; | ||
| 646 | comptime assert(@TypeOf(cond.state) == u64); | ||
| 647 | const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); | ||
| 648 | const cond_state = &ints[0]; | ||
| 649 | const cond_epoch = &ints[1]; | ||
| 650 | const one_waiter = 1; | ||
| 651 | const waiter_mask = 0xffff; | ||
| 652 | const one_signal = 1 << 16; | ||
| 653 | const signal_mask = 0xffff << 16; | ||
| 654 | var state = cond_state.load(.monotonic); | ||
| 655 | while (true) { | ||
| 656 | const waiters = (state & waiter_mask) / one_waiter; | ||
| 657 | const signals = (state & signal_mask) / one_signal; | ||
| 658 | |||
| 659 | // Reserves which waiters to wake up by incrementing the signals count. | ||
| 660 | // Therefore, the signals count is always less than or equal to the waiters count. | ||
| 661 | // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters. | ||
| 662 | const wakeable = waiters - signals; | ||
| 663 | if (wakeable == 0) { | ||
| 664 | return; | ||
| 665 | } | ||
| 666 | |||
| 667 | const to_wake = switch (notify) { | ||
| 668 | .one => 1, | ||
| 669 | .all => wakeable, | ||
| 670 | }; | ||
| 671 | |||
| 672 | // Reserve the amount of waiters to wake by incrementing the signals count. | ||
| 673 | // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. | ||
| 674 | const new_state = state + (one_signal * to_wake); | ||
| 675 | state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { | ||
| 676 | // Wake up the waiting threads we reserved above by changing the epoch value. | ||
| 677 | // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it. | ||
| 678 | // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption. | ||
| 679 | // | ||
| 680 | // Release barrier ensures the signal being added to the state happens before the epoch is changed. | ||
| 681 | // If not, the waiting thread could potentially deadlock from missing both the state and epoch change: | ||
| 682 | // | ||
| 683 | // - T2: UPDATE(&epoch, 1) (reordered before the state change) | ||
| 684 | // - T1: e = LOAD(&epoch) | ||
| 685 | // - T1: s = LOAD(&state) | ||
| 686 | // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) | ||
| 687 | // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) | ||
| 688 | _ = cond_epoch.fetchAdd(1, .release); | ||
| 689 | std.Thread.Futex.wake(cond_epoch, to_wake); | ||
| 690 | return; | ||
| 691 | }; | ||
| 692 | } | ||
| 522 | } | 693 | } |
| 523 | 694 | ||
| 524 | pub fn createFile( | 695 | fn createFile( |
| 525 | userdata: ?*anyopaque, | 696 | userdata: ?*anyopaque, |
| 526 | dir: std.fs.Dir, | 697 | dir: std.fs.Dir, |
| 527 | sub_path: []const u8, | 698 | sub_path: []const u8, |
| ... | @@ -532,7 +703,7 @@ pub fn createFile( | ... | @@ -532,7 +703,7 @@ pub fn createFile( |
| 532 | return dir.createFile(sub_path, flags); | 703 | return dir.createFile(sub_path, flags); |
| 533 | } | 704 | } |
| 534 | 705 | ||
| 535 | pub fn openFile( | 706 | fn openFile( |
| 536 | userdata: ?*anyopaque, | 707 | userdata: ?*anyopaque, |
| 537 | dir: std.fs.Dir, | 708 | dir: std.fs.Dir, |
| 538 | sub_path: []const u8, | 709 | sub_path: []const u8, |
| ... | @@ -543,13 +714,13 @@ pub fn openFile( | ... | @@ -543,13 +714,13 @@ pub fn openFile( |
| 543 | return dir.openFile(sub_path, flags); | 714 | return dir.openFile(sub_path, flags); |
| 544 | } | 715 | } |
| 545 | 716 | ||
| 546 | pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { | 717 | fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { |
| 547 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | 718 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); |
| 548 | _ = pool; | 719 | _ = pool; |
| 549 | return file.close(); | 720 | return file.close(); |
| 550 | } | 721 | } |
| 551 | 722 | ||
| 552 | pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { | 723 | fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { |
| 553 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | 724 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); |
| 554 | try pool.checkCancel(); | 725 | try pool.checkCancel(); |
| 555 | return switch (offset) { | 726 | return switch (offset) { |
| ... | @@ -558,7 +729,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std | ... | @@ -558,7 +729,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std |
| 558 | }; | 729 | }; |
| 559 | } | 730 | } |
| 560 | 731 | ||
| 561 | pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { | 732 | fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { |
| 562 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | 733 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); |
| 563 | try pool.checkCancel(); | 734 | try pool.checkCancel(); |
| 564 | return switch (offset) { | 735 | return switch (offset) { |