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.
const Node = struct
const 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()];
}
}