libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
Loading...
Searching...
No Matches
deque.h
1/*-
2 * Copyright (c) 2026, The XTC Project
3 * Use of this source code is governed by the ISC License.
4 *
5 * src/inc/deque.h
6 * A bounded Chase-Lev work-stealing deque. Single-owner pushes
7 * and pops on the bottom; multiple thieves CAS-steal from the
8 * top. See:
9 * Chase, D. & Lev, Y. "Dynamic Circular Work-Stealing Deque",
10 * SPAA 2005.
11 *
12 * M5 ships a fixed-capacity variant: callers fall back to a
13 * mutex-protected slow-path queue when push fails with
14 * XTC_E_AGAIN. M5.5 may add CAS-based growth.
15 *
16 * The deque stores `void *` and is generic; the run-queue uses
17 * xtc_task_t* by convention.
18 */
19
20#ifndef XTC_DEQUE_H
21#define XTC_DEQUE_H
22
23#include <stdatomic.h>
24#include <stdint.h>
25
26#include "xtc.h"
27
28/*
29 * Capacity must be a power of two. 256 is generous for a per-loop
30 * run queue; PG-style backends typically have <128 in flight.
31 * Overridable (bounded model checking builds it small -- the
32 * algorithm is capacity-agnostic, indices are unbounded and masked at
33 * access, so a small cap exercises identical logic in a tractable
34 * state space; see test/cbmc/deque_harness.c).
35 */
36#ifndef XTC_DEQUE_CAP
37#define XTC_DEQUE_CAP 256
38#endif
39#define XTC_DEQUE_MASK (XTC_DEQUE_CAP - 1)
40
41typedef struct xtc_deque {
42 /*
43 * top and bottom are 64-bit indices; we mask them by the
44 * capacity at slot access. Using 64-bit indices avoids any
45 * wrap concern inside the lifetime of a process.
46 *
47 * - `bottom` is owned by the owner thread; only it writes.
48 * - `top` is read by everyone, written via CAS by thieves
49 * (to claim a slot) and by the owner (when popping the
50 * last item to break a tie with a thief).
51 */
52 _Atomic int64_t top;
53 _Atomic int64_t bottom;
54 _Atomic(void *) buf[XTC_DEQUE_CAP];
56
57static inline void
58xtc_deque_init(xtc_deque_t *d)
59{
60 int i;
61 atomic_store_explicit(&d->top, 0, memory_order_relaxed);
62 atomic_store_explicit(&d->bottom, 0, memory_order_relaxed);
63 for (i = 0; i < XTC_DEQUE_CAP; i++)
64 atomic_store_explicit(&d->buf[i], NULL, memory_order_relaxed);
65}
66
67/*
68 * Owner-side push. Returns XTC_OK on success, XTC_E_AGAIN if the
69 * deque is full (caller must use the slow-path overflow queue).
70 */
71static inline int
72xtc_deque_push(xtc_deque_t *d, void *v)
73{
74 int64_t b = atomic_load_explicit(&d->bottom, memory_order_relaxed);
75 int64_t t = atomic_load_explicit(&d->top, memory_order_acquire);
76 if (b - t >= XTC_DEQUE_CAP) return XTC_E_AGAIN;
77 atomic_store_explicit(&d->buf[b & XTC_DEQUE_MASK], v,
78 memory_order_relaxed);
79 /* A release store, not fence + relaxed store: the same ordering, but
80 * ThreadSanitizer does not model standalone fences, so the fence form
81 * hid the push->steal edge and every write a thief makes to a stolen
82 * task (q_next, inbox_next) was reported as a race. */
83 atomic_store_explicit(&d->bottom, b + 1, memory_order_release);
84 return XTC_OK;
85}
86
87/*
88 * Owner-side pop (LIFO). Returns NULL if the deque is empty.
89 *
90 * The tricky case is a single-element deque: both the owner and a
91 * thief can race for it. We resolve via CAS on `top`: whoever wins
92 * gets the item; the loser sees empty.
93 */
94static inline void *
95xtc_deque_pop(xtc_deque_t *d)
96{
97 int64_t b = atomic_load_explicit(&d->bottom, memory_order_relaxed) - 1;
98 int64_t t;
99 void *x;
100
101 atomic_store_explicit(&d->bottom, b, memory_order_relaxed);
102 atomic_thread_fence(memory_order_seq_cst);
103 t = atomic_load_explicit(&d->top, memory_order_relaxed);
104
105 if (t > b) {
106 /* Empty. */
107 atomic_store_explicit(&d->bottom, t, memory_order_relaxed);
108 return NULL;
109 }
110
111 x = atomic_load_explicit(&d->buf[b & XTC_DEQUE_MASK],
112 memory_order_relaxed);
113 if (t < b) return x; /* uncontested */
114
115 /* t == b: last element, race with thief. */
116 if (!atomic_compare_exchange_strong_explicit(
117 &d->top, &t, t + 1,
118 memory_order_seq_cst, memory_order_relaxed))
119 x = NULL; /* lost the race */
120 atomic_store_explicit(&d->bottom, b + 1, memory_order_relaxed);
121 return x;
122}
123
124/*
125 * Thief-side steal (FIFO). Returns NULL on empty or on losing the
126 * CAS race with another thief or with the owner's pop.
127 */
128static inline void *
129xtc_deque_steal(xtc_deque_t *d)
130{
131 int64_t t = atomic_load_explicit(&d->top, memory_order_acquire);
132 int64_t b;
133 void *x;
134
135 atomic_thread_fence(memory_order_seq_cst);
136 b = atomic_load_explicit(&d->bottom, memory_order_acquire);
137
138 if (t >= b) return NULL; /* empty */
139 x = atomic_load_explicit(&d->buf[t & XTC_DEQUE_MASK],
140 memory_order_relaxed);
141 if (!atomic_compare_exchange_strong_explicit(
142 &d->top, &t, t + 1,
143 memory_order_seq_cst, memory_order_relaxed))
144 return NULL; /* lost race */
145 return x;
146}
147
148/*
149 * Owner-side FIFO take: the oldest item, from the steal end. The run
150 * queue uses this instead of the LIFO xtc_deque_pop so a task that
151 * re-queues itself (XTC_TASK_RESCHED) goes behind everything already
152 * queued rather than straight back to the front. It is the owner
153 * stealing from itself: xtc_deque_steal is multi-consumer safe, so the
154 * owner's take and peer thieves are arbitrated by the same CAS on top.
155 * A NULL from steal on a non-empty deque means another consumer won the
156 * slot, so retry; each lost race is someone else's progress.
157 */
158static inline void *
159xtc_deque_take(xtc_deque_t *d)
160{
161 void *x;
162
163 while (atomic_load_explicit(&d->bottom, memory_order_relaxed) >
164 atomic_load_explicit(&d->top, memory_order_acquire))
165 if ((x = xtc_deque_steal(d)) != NULL)
166 return x;
167 return NULL;
168}
169
170/*
171 * Approximate length. May return slightly stale values; useful for
172 * stealing heuristics ("which loop is busiest?").
173 */
174static inline int64_t
175xtc_deque_len(const xtc_deque_t *d)
176{
177 int64_t b = atomic_load_explicit(&d->bottom, memory_order_acquire);
178 int64_t t = atomic_load_explicit(&d->top, memory_order_acquire);
179 return b - t;
180}
181
182#endif /* XTC_DEQUE_H */