feature. See also
. The project being documented here (as the example) is the Zig library itself.
Threaded.parking_futex
const parking_futex = struct
File
Code
const parking_futex = struct {
comptime {
assert(use_parking_futex);
}
const Bucket = struct {
num_waiters: std.atomic.Value(u32),
mutex: ParkingMutex,
waiters: std.DoublyLinkedList,
_: void align(std.atomic.cache_line) = {},
const init: Bucket = .{ .num_waiters = .init(0), .mutex = .init, .waiters = .{} };
};
const Waiter = struct {
node: std.DoublyLinkedList.Node,
address: usize,
tid: std.Thread.Id,
thread_status: *std.atomic.Value(Thread.Status),
unpark_flag: if (need_unpark_flag) *UnparkFlag else void,
};
fn bucketForAddress(address: usize) *Bucket {
const global = struct {
var buckets: [256]Bucket = @splat(.init);
};
// values across a range, giving a poor, but extremely quick to compute, hash.
// This literal is the rounded value of '2^64 / phi' (where 'phi' is the golden ratio). The
// shift then converts it to '2^b / phi', where 'b' is the pointer bit width.
const fibonacci_multiplier = 0x9E3779B97F4A7C15 >> (64 - @bitSizeOf(usize));
const hashed = address *% fibonacci_multiplier;
comptime assert(std.math.isPowerOfTwo(global.buckets.len));
const index = hashed >> (@bitSizeOf(usize) - @ctz(global.buckets.len));
return &global.buckets[index];
}
fn wait(ptr: *const u32, expect: u32, uncancelable: bool, timeout: Io.Timeout) Io.Cancelable!void {
const bucket = bucketForAddress(@intFromPtr(ptr));
const opt_thread = Thread.current;
const self_tid = if (opt_thread) |thread| thread.id else std.Thread.getCurrentId();
var waiter: Waiter = .{
.node = undefined,
.address = @intFromPtr(ptr),
.tid = self_tid,
.thread_status = undefined,
.unpark_flag = undefined,
};
var status_buf: std.atomic.Value(Thread.Status) = undefined;
var unpark_flag_buf: UnparkFlag = unpark_flag_init;
{
bucket.mutex.lock();
defer bucket.mutex.unlock();
_ = bucket.num_waiters.fetchAdd(1, .acquire);
if (@atomicLoad(u32, ptr, .monotonic) != expect) {
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
return;
}
// certain that we're actually going to park.
waiter.thread_status, waiter.unpark_flag = status: {
cancelable: {
if (uncancelable) break :cancelable;
const thread = opt_thread orelse break :cancelable;
switch (thread.cancel_protection) {
.blocked => break :cancelable,
.unblocked => {},
}
thread.futex_waiter = &waiter;
const old_status = thread.status.fetchOr(
.{ .cancelation = @fromBackingInt(@intCast(0b001)), .awaitable = .null },
.release,
);
switch (old_status.cancelation) {
.none => {},
.canceling => {
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
return error.Canceled;
},
.canceled => break :cancelable,
.parked => unreachable,
.blocked => unreachable,
.blocked_alertable => unreachable,
.blocked_alertable_canceling => unreachable,
.blocked_canceling => unreachable,
}
break :status .{ &thread.status, if (need_unpark_flag) &thread.unpark_flag };
}
// `status_buf.awaitable` is irrelevant because this is only visible to futex code,
// while only cancelation cares about `awaitable`.
status_buf.raw = .{ .cancelation = .parked, .awaitable = .null };
break :status .{ &status_buf, if (need_unpark_flag) &unpark_flag_buf };
};
bucket.waiters.append(&waiter.node);
}
if (park(timeout, ptr, waiter.unpark_flag)) {
// `.none` or `.canceling`. In either case, they've already removed `waiter` from
// `bucket`, so we have nothing more to do!
} else |err| switch (err) {
error.Timeout => {
const old_status = waiter.thread_status.fetchAnd(
.{ .cancelation = @fromBackingInt(@intCast(0b110)), .awaitable = .all_ones },
.monotonic,
);
switch (old_status.cancelation) {
.parked => {
// New status is `.none`.
bucket.mutex.lock();
defer bucket.mutex.unlock();
bucket.waiters.remove(&waiter.node);
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
},
.none, .canceling => {
// to unpark us. Whoever did that will remove us from `bucket`. Wait for
// that (and drop the unpark request in doing so).
// New status is `.none` or `.canceling` respectively.
park(.none, ptr, waiter.unpark_flag) catch |e| switch (e) {
error.Timeout => unreachable,
};
},
.canceled => unreachable,
.blocked => unreachable,
.blocked_alertable => unreachable,
.blocked_canceling => unreachable,
.blocked_alertable_canceling => unreachable,
}
},
}
}
fn wake(ptr: *const u32, max_waiters: u32) void {
if (max_waiters == 0) return;
const bucket = bucketForAddress(@intFromPtr(ptr));
// load, but that doesn't exist in the C11 memory model, so emulate it with a non-mutating rmw.
if (bucket.num_waiters.fetchAdd(0, .release) == 0) {
@branchHint(.likely);
return;
}
// of the critical section. This forms a singly-linked list of waiters using `Waiter.node.next`.
var waking_head: ?*std.DoublyLinkedList.Node = null;
{
bucket.mutex.lock();
defer bucket.mutex.unlock();
var num_removed: u32 = 0;
var it = bucket.waiters.first;
while (num_removed < max_waiters) {
const waiter: *Waiter = @fieldParentPtr("node", it orelse break);
it = waiter.node.next;
if (waiter.address != @intFromPtr(ptr)) continue;
const old_status = waiter.thread_status.fetchAnd(
.{ .cancelation = @fromBackingInt(@intCast(0b110)), .awaitable = .all_ones },
.monotonic,
);
switch (old_status.cancelation) {
.parked => {},
.none => continue,
.canceling => continue,
.canceled => unreachable,
.blocked => unreachable,
.blocked_alertable => unreachable,
.blocked_alertable_canceling => unreachable,
.blocked_canceling => unreachable,
}
bucket.waiters.remove(&waiter.node);
waiter.node.next = waking_head;
waking_head = &waiter.node;
num_removed += 1;
}
_ = bucket.num_waiters.fetchSub(num_removed, .monotonic);
}
var unpark_buf: [128]UnparkTid = undefined;
var unpark_len: usize = 0;
while (waking_head) |node| {
waking_head = node.next;
const waiter: *Waiter = @fieldParentPtr("node", node);
unpark_buf[unpark_len] = waiter.tid;
if (need_unpark_flag) setUnparkFlag(waiter.unpark_flag);
unpark_len += 1;
if (unpark_len == unpark_buf.len) {
unpark(&unpark_buf, ptr);
unpark_len = 0;
}
}
if (unpark_len > 0) {
unpark(unpark_buf[0..unpark_len], ptr);
}
}
fn removeCanceledWaiter(waiter: *Waiter) void {
const bucket = bucketForAddress(waiter.address);
bucket.mutex.lock();
defer bucket.mutex.unlock();
bucket.waiters.remove(&waiter.node);
assert(bucket.num_waiters.fetchSub(1, .monotonic) > 0);
}
}