mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-16 13:09:24 +00:00
93 lines
3.3 KiB
Nim
93 lines
3.3 KiB
Nim
## Sharded, mutex-guarded MPSC ingress for `ptr FFIThreadRequest`: N intrusive
|
|
## FIFOs (one per producer) spread lock contention; the request is its own node
|
|
## so enqueue never touches a Nim GC heap. Unbounded — submit never blocks.
|
|
|
|
import std/[atomics, locks]
|
|
import ./ffi_thread_request
|
|
|
|
const
|
|
RequestQueueCount* = 16
|
|
## Independent ingress queues; ≥ concurrent producer count keeps collisions low.
|
|
QueuePadBytes = 192
|
|
## Pads each queue past a cache line (128B on Apple silicon) to avoid false
|
|
## sharing between adjacent queues.
|
|
|
|
static:
|
|
# `myQueueIndex` masks with `and`, so the count must be a power of two.
|
|
doAssert (RequestQueueCount and (RequestQueueCount - 1)) == 0,
|
|
"RequestQueueCount must be a power of two"
|
|
|
|
type
|
|
RequestQueue = object
|
|
lock: Lock
|
|
head: ptr FFIThreadRequest ## consumer pops here (oldest)
|
|
tail: ptr FFIThreadRequest ## producers append here (newest)
|
|
pad: array[QueuePadBytes, byte]
|
|
|
|
RequestQueueBank* = object
|
|
queues: array[RequestQueueCount, RequestQueue]
|
|
|
|
var gRequestQueue {.threadvar.}: int
|
|
var gRequestQueueAssigned {.threadvar.}: bool
|
|
var gRequestQueueCounter: Atomic[int]
|
|
## Round-robins producers onto distinct queues on first use so they fill evenly.
|
|
|
|
proc myQueueIndex(): int {.raises: [].} =
|
|
if not gRequestQueueAssigned:
|
|
gRequestQueue = gRequestQueueCounter.fetchAdd(1)
|
|
gRequestQueueAssigned = true
|
|
return gRequestQueue and (RequestQueueCount - 1)
|
|
|
|
proc initRequestQueue*(bank: var RequestQueueBank) {.raises: [].} =
|
|
for queue in bank.queues.mitems:
|
|
queue.lock.initLock()
|
|
queue.head = nil
|
|
queue.tail = nil
|
|
|
|
proc deinitRequestQueue*(bank: var RequestQueueBank) {.raises: [].} =
|
|
## Both producers and consumer must have stopped. Frees any still-queued request
|
|
## (e.g. one raced in after the final drain) so a teardown race leaks nothing.
|
|
for queue in bank.queues.mitems:
|
|
var request = queue.head
|
|
while not request.isNil():
|
|
let nextRequest = request[].next
|
|
deleteRequest(request)
|
|
request = nextRequest
|
|
queue.head = nil
|
|
queue.tail = nil
|
|
queue.lock.deinitLock()
|
|
|
|
proc pushRequest*(
|
|
bank: var RequestQueueBank, request: ptr FFIThreadRequest
|
|
): bool {.raises: [].} =
|
|
## Append `request` to this thread's queue (takes ownership). True only when the
|
|
## queue was empty — the one push that must wake the sleeping consumer.
|
|
request[].next = nil
|
|
let idx = myQueueIndex()
|
|
withLock bank.queues[idx].lock:
|
|
let wasEmpty = bank.queues[idx].tail.isNil()
|
|
if bank.queues[idx].tail.isNil():
|
|
bank.queues[idx].head = request
|
|
else:
|
|
bank.queues[idx].tail[].next = request
|
|
bank.queues[idx].tail = request
|
|
return wasEmpty
|
|
|
|
proc mergeQueues*(bank: var RequestQueueBank): ptr FFIThreadRequest {.raises: [].} =
|
|
## Single-consumer: splice every queue into one chain and reset them. Caller owns
|
|
## the chain and must read each `next` before dispatch (dispatch frees the request).
|
|
var head: ptr FFIThreadRequest = nil
|
|
var tail: ptr FFIThreadRequest = nil
|
|
for queue in bank.queues.mitems:
|
|
withLock queue.lock:
|
|
let h = queue.head
|
|
if not h.isNil():
|
|
if head.isNil():
|
|
head = h
|
|
else:
|
|
tail[].next = h
|
|
tail = queue.tail
|
|
queue.head = nil
|
|
queue.tail = nil
|
|
return head
|