code wiki / (root) / nx_chan.nx

nx_chan.nx source

↩ module page · 257 lines · 8911 B

1// nx_chan.nx -- Vyukov MPMC bounded lock-free queue (i64 messages). 2// 3// Multi-Producer Multi-Consumer queue based on Dmitry Vyukov's 4// 2010 design ("Bounded MPMC queue", 1024cores.net). Each cell 5// carries a sequence number; producer sees `seq == enq_pos` to 6// claim a slot, consumer sees `seq == deq_pos + 1` to drain. 7// CAS bumps the position counter. Wait-free under no contention, 8// lock-free under contention. 9// 10// Properties: 11// * MPMC: any number of producers + any number of consumers 12// * Bounded: capacity rounded up to power of 2 (mask-wrap) 13// * Lock-free: no spin locks, no futex waits in fast path 14// * FIFO order globally 15// 16// Compose-against: [[atomic_intrinsics_real_amo]] for CAS+FAA; 17// [[thread_clone_native_trampoline]] for the worker threads; 18// [[nx_hw_dynamic_probes]] for sizing producer/consumer count. 19// 20// Predecessor nx_channel.nx remains for SPSC code (smaller cell 21// footprint, no per-cell seq); use nx_chan.nx whenever multiple 22// threads can produce or consume. 23// 24// Reference: Vyukov D., "Bounded MPMC queue", 2010 25// (independent rederive from algorithm description; no code copied). 26 27// nx_safety_envelope: 28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 29// sil_target: SIL1 30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 31// verdict: NOT_YET_EVALUATED 32 33import "nx_syscalls.nx" 34import "nx_atom.nx" 35import "nx_thread.nx" 36 37// One cell of the ring buffer: a sequence counter + the value. 38// Sequence number tracks "expected enqueue position" for the slot; 39// after a successful enqueue it becomes "expected dequeue position". 40struct NxChanCell { 41 seq: i64, 42 value: i64, 43} 44 45const NX_CHAN_CELL_BYTES: i64 = 16 46 47struct NxChan { 48 buf: *u8, // base of cell array 49 mask: i64, // capacity - 1; cap is power of 2 50 enq_pos: i64, // monotonic enqueue counter 51 deq_pos: i64, // monotonic dequeue counter 52} 53 54const NX_CHAN_HEADER_BYTES: i64 = 64 55 56// Compute address of cell[idx]. idx is wrapped via the mask. 57func _nx_chan_cell(c: *NxChan, idx: i64) -> *NxChanCell { 58 let off: i64 = (idx & c.mask) * NX_CHAN_CELL_BYTES 59 return ((c.buf as i64) + off) as *NxChanCell 60} 61 62// Allocate a new MPMC channel. cap_hint is rounded up to the next 63// power of 2; minimum 2. Each cell starts with seq == its index so 64// the very first producer at position 0 sees seq == 0 and proceeds. 65func nx_chan_new(cap_hint: i64) -> *NxChan { 66 var cap: i64 = 1 67 while cap < cap_hint { cap = cap * 2 } 68 if cap < 2 { cap = 2 } 69 70 let total: i64 = NX_CHAN_HEADER_BYTES + cap * NX_CHAN_CELL_BYTES 71 let raw: *u8 = sys_mmap(total) 72 let c: *NxChan = raw as *NxChan 73 c.buf = ((raw as i64) + NX_CHAN_HEADER_BYTES) as *u8 74 c.mask = cap - 1 75 c.enq_pos = 0 76 c.deq_pos = 0 77 78 // Seed seq numbers: cell[i].seq = i. 79 var i: i64 = 0 80 while i < cap { 81 let cell: *NxChanCell = _nx_chan_cell(c, i) 82 cell.seq = i 83 cell.value = 0 84 i = i + 1 85 } 86 return c 87} 88 89// Non-blocking send. Returns 1 on success, 0 when full. 90// 91// Algorithm: 92// pos = atomic_load(enq_pos) 93// loop: 94// cell = &buf[pos & mask] 95// seq = atomic_load(cell.seq) 96// diff = seq - pos 97// if diff == 0: try CAS enq_pos pos -> pos+1 98// on success: cell.value = v; atomic_store(cell.seq, pos+1) 99// elif diff < 0: queue full -- return 0 100// else: another producer raced ahead -- reload pos and retry 101func nx_chan_try_send(c: *NxChan, v: i64) -> i64 { 102 let enq_addr: *i64 = ((c as i64) + 16) as *i64 103 var pos: i64 = __atomic_load_i64(enq_addr, NX_MO_RELAXED) 104 var done: i64 = 0 105 var result: i64 = 0 106 while done == 0 { 107 let cell: *NxChanCell = _nx_chan_cell(c, pos) 108 let seq_addr: *i64 = (cell as i64) as *i64 109 let seq: i64 = __atomic_load_i64(seq_addr, NX_MO_ACQUIRE) 110 let diff: i64 = seq - pos 111 if diff == 0 { 112 let won: i64 = __atomic_cas_i64(enq_addr, pos, pos + 1, NX_MO_RELAXED) 113 if won == 1 { 114 cell.value = v 115 // ⚠Bound-and-discarded for the same reason as nx_atom_store_i64: nx_cc refuses a bare 116 // intrinsic statement, and an atomic store has no result worth using. `done` is set to 1 117 // unconditionally on the next line, so consuming the value here cannot change behaviour. 118 let published: i64 = __atomic_store_i64(seq_addr, pos + 1, NX_MO_RELEASE) 119 if published != 0 { done = 1 } 120 done = 1 121 result = 1 122 } 123 } else { 124 if diff < 0 { 125 done = 1 126 result = 0 127 } else { 128 pos = __atomic_load_i64(enq_addr, NX_MO_RELAXED) 129 } 130 } 131 } 132 return result 133} 134 135// Non-blocking receive. Returns 1 + writes message to *out on 136// success; returns 0 when empty. 137func nx_chan_try_recv(c: *NxChan, out: *i64) -> i64 { 138 let deq_addr: *i64 = ((c as i64) + 24) as *i64 139 var pos: i64 = __atomic_load_i64(deq_addr, NX_MO_RELAXED) 140 var done: i64 = 0 141 var result: i64 = 0 142 while done == 0 { 143 let cell: *NxChanCell = _nx_chan_cell(c, pos) 144 let seq_addr: *i64 = (cell as i64) as *i64 145 let seq: i64 = __atomic_load_i64(seq_addr, NX_MO_ACQUIRE) 146 let diff: i64 = seq - (pos + 1) 147 if diff == 0 { 148 let won: i64 = __atomic_cas_i64(deq_addr, pos, pos + 1, NX_MO_RELAXED) 149 if won == 1 { 150 *out = cell.value 151 // Free the slot for the next producer one full lap later. 152 // Bound-and-discarded exactly as in nx_chan_try_send above; `done` is set unconditionally 153 // on the next line, so consuming the store's result cannot change behaviour. 154 let freed: i64 = __atomic_store_i64(seq_addr, pos + c.mask + 1, NX_MO_RELEASE) 155 if freed != 0 { done = 1 } 156 done = 1 157 result = 1 158 } 159 } else { 160 if diff < 0 { 161 done = 1 162 result = 0 163 } else { 164 pos = __atomic_load_i64(deq_addr, NX_MO_RELAXED) 165 } 166 } 167 } 168 return result 169} 170 171// Blocking send -- spins on full, yields the CPU between tries so a 172// blocked sender doesn't starve drainers on single-core systems. 173func nx_chan_send(c: *NxChan, v: i64) -> i64 { 174 var sent: i64 = 0 175 while sent == 0 { 176 sent = nx_chan_try_send(c, v) 177 if sent == 0 { nx_thread_yield() } 178 } 179 return 0 180} 181 182// Blocking receive -- spins on empty with cooperative yield. 183func nx_chan_recv(c: *NxChan) -> i64 { 184 let out_raw: *u8 = sys_mmap(16) 185 let out: *i64 = out_raw as *i64 186 var got: i64 = 0 187 while got == 0 { 188 got = nx_chan_try_recv(c, out) 189 if got == 0 { nx_thread_yield() } 190 } 191 return *out 192} 193 194// Approximate length: enq_pos - deq_pos. Best-effort under contention. 195func nx_chan_len(c: *NxChan) -> i64 { 196 let enq_addr: *i64 = ((c as i64) + 16) as *i64 197 let deq_addr: *i64 = ((c as i64) + 24) as *i64 198 let e: i64 = __atomic_load_i64(enq_addr, NX_MO_RELAXED) 199 let d: i64 = __atomic_load_i64(deq_addr, NX_MO_RELAXED) 200 return e - d 201} 202 203// ---- self-test --------------------------------------------------- 204 205func main() -> i64 { 206 let c: *NxChan = nx_chan_new(8) 207 if c.mask != 7 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 208 if nx_chan_len(c) != 0 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 209 210 // Send 5 values; should succeed. 211 var i: i64 = 0 212 while i < 5 { 213 if nx_chan_try_send(c, 100 + i) != 1 { 214 return __syscall(93, 10, 0, 0, 0, 0, 0) 215 } 216 i = i + 1 217 } 218 if nx_chan_len(c) != 5 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 219 220 // Recv first 3 values; check FIFO order. 221 let out_raw: *u8 = sys_mmap(16) 222 let out: *i64 = out_raw as *i64 223 var j: i64 = 0 224 while j < 3 { 225 if nx_chan_try_recv(c, out) != 1 { 226 return __syscall(93, 20, 0, 0, 0, 0, 0) 227 } 228 if *out != 100 + j { 229 return __syscall(93, 21, 0, 0, 0, 0, 0) 230 } 231 j = j + 1 232 } 233 if nx_chan_len(c) != 2 { return __syscall(93, 22, 0, 0, 0, 0, 0) } 234 235 // Drain remainder. 236 while nx_chan_try_recv(c, out) == 1 { } 237 if nx_chan_len(c) != 0 { return __syscall(93, 30, 0, 0, 0, 0, 0) } 238 239 // Fill to capacity then verify next try_send fails (full). 240 var k: i64 = 0 241 while k < 8 { 242 if nx_chan_try_send(c, 200 + k) != 1 { 243 return __syscall(93, 40, 0, 0, 0, 0, 0) 244 } 245 k = k + 1 246 } 247 if nx_chan_try_send(c, 999) != 0 { 248 return __syscall(93, 41, 0, 0, 0, 0, 0) 249 } 250 251 // Drain again. 252 var n: i64 = 0 253 while nx_chan_try_recv(c, out) == 1 { n = n + 1 } 254 if n != 8 { return __syscall(93, 42, 0, 0, 0, 0, 0) } 255 256 return 0 257}