const Node = struct size: SizeOnly meant to be accessed indirectly via the methods supplied by this type, except if the node is owned by the thread accessing it. Must always be an even number to accommodate resize bit.
end_index: usizeAny increase of end_index has to use acquire semantics; any decrease of end_index that invalidates (formerly) active allocations has to use release semantics. This guarantees that all accesses to memory that's about to be freed happen-before the free is published. Since size can only grow and never shrink, memory access depending on any end_index <= any size can never be OOB.
next: ?*NodeThis field should only be accessed if the node is owned by the thread accessing it.
fn loadBuf(node: *Node) []u8fn beginResize(node: *Node) ?[]u8Returns allocated slice or null if node is already (being) resized.
fn endResize(node: *Node, size: usize, prev_size: usize) voidfn allocatedSliceUnsafe(node: *Node) []u8Not threadsafe.
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277const Node = struct {
/// Only meant to be accessed indirectly via the methods supplied by this type,
/// except if the node is owned by the thread accessing it.
/// Must always be an even number to accommodate `resize` bit.
size: Size,
/// Any increase of `end_index` has to use acquire semantics;
/// any decrease of `end_index` that invalidates (formerly) active allocations
/// has to use release semantics.
/// This guarantees that all accesses to memory that's about to be freed
/// happen-before the free is published.
/// Since `size` can only grow and never shrink, memory access depending on
/// any `end_index` <= any `size` can never be OOB.
end_index: usize,
/// This field should only be accessed if the node is owned by the thread
/// accessing it.
next: ?*Node,
const Size = packed struct(usize) {
resizing: bool,
_: @Int(.unsigned, @bitSizeOf(usize) - 1) = 0,
fn fromInt(int: usize) Size {
assert(int >= @sizeOf(Node));
const size: Size = @bitCast(int);
assert(!size.resizing);
return size;
}
fn toInt(size: Size) usize {
var int = size;
int.resizing = false;
return @bitCast(int);
}
comptime {
assert(Size{ .resizing = true } == @as(Size, @bitCast(@as(usize, 1))));
}
};
fn loadBuf(node: *Node) []u8 {
// `size` can only ever grow, so the buffer returned by this function is
// always valid memory.
const size = @atomicLoad(Size, &node.size, .monotonic);
return @as([*]u8, @ptrCast(node))[0..size.toInt()][@sizeOf(Node)..];
}
/// Returns allocated slice or `null` if node is already (being) resized.
fn beginResize(node: *Node) ?[]u8 {
const size = @atomicRmw(Size, &node.size, .Or, .{ .resizing = true }, .acquire); // syncs with release in `endResize`
if (size.resizing) return null;
return @as([*]u8, @ptrCast(node))[0..size.toInt()];
}
fn endResize(node: *Node, size: usize, prev_size: usize) void {
assert(size >= prev_size); // nodes must not shrink
assert(@atomicLoad(Size, &node.size, .unordered).toInt() == prev_size);
return @atomicStore(Size, &node.size, .fromInt(size), .release); // syncs with acquire in `beginResize`
}
/// Not threadsafe.
fn allocatedSliceUnsafe(node: *Node) []u8 {
return @as([*]u8, @ptrCast(node))[0..node.size.toInt()];
}
}
Concurrent accesses to node pointers generally have to have acquire/release semantics to guarantee that newly allocated notes are in a valid state when being inserted into a list. Exceptions are possible, e.g. a cmpxchg loop that never accesses the node returned on failure can use monotonic semantics on failure, but must still use release semantics on success to protect the node it's trying to push.