Skip to content

Commit e757d13

Browse files
committed
feat: add lock-free type checking and CI improvements
Add compile-time safety checks: - Queue item types checked with isLockFree on arc/orc - ref types error by default (use spinlocks for refcounting) - Use -d:allowNonLockFreeQueueItems to opt-out Update CI/test infrastructure: - Test with arc, orc, refc memory managers - Test with -d:nimEnforceLockFreeAtomics flag - Stress tests updated with MM variants Add documentation: - Thread safety section in README - Slot-ownership typestates documentation Bump version to 3.2.0.
1 parent 9ad7c5a commit e757d13

11 files changed

Lines changed: 472 additions & 11 deletions

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ jobs:
6060
export SANITIZE_ADDRESS=${{ matrix.sanitize-address }}
6161
. "${HOME}/.asdf/asdf.sh"
6262
nimble develop -y
63+
echo "::group::Run test suite (includes arc, orc, refc, lock-free enforcement)"
6364
nimble test
65+
echo "::endgroup::"
66+
echo "::group::Run examples"
6467
nimble examples
68+
echo "::endgroup::"
6569
6670
test_non_x86:
6771
name: Test nim-${{ matrix.nim-version }} / debian-buster / ${{ matrix.arch }}

CHANGELOG.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [3.2.0] - 2025-12-18
11+
1012
### Added
1113

14+
- Unbounded queue implementations with DEBRA epoch-based reclamation
15+
- `UnboundedSipsic` - Single-producer, single-consumer (no DEBRA needed)
16+
- `UnboundedSipmuc` - Single-producer, multi-consumer
17+
- `UnboundedMupsic` - Multi-producer, single-consumer
18+
- `UnboundedMupmuc` - Multi-producer, multi-consumer
19+
- Typestate-enforced push/pop operations for all unbounded queues
20+
- Compile-time lock-free type checking for queue item types
21+
- Errors on `ref` types with arc/orc (uses spinlocks for refcounting)
22+
- Use `-d:allowNonLockFreeQueueItems` to opt-out
23+
- Thread safety documentation in README
24+
- Slot-ownership typestates documentation
25+
- CI testing with multiple memory managers (arc, orc, refc)
26+
- CI testing with `-d:nimEnforceLockFreeAtomics` flag
27+
1228
### Changed
1329

14-
### Removed
30+
- Test suite now runs with arc, orc, refc memory managers
31+
- Test suite now verifies lock-free enforcement
32+
- Stress tests updated with MM variants
33+
- Dependencies updated: `typestates >= 0.3.1`, `debra >= 0.2.0`
1534

1635
## [3.1.0] - 2024-09-28
1736

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,61 @@ API documentation: https://elijahr.github.io/lockfreequeues
2626
nimble install lockfreequeues
2727
```
2828

29+
## Thread Safety and Lock-Free Guarantees
30+
31+
### Item Type Requirements
32+
33+
By default, lockfreequeues requires that queue item types are lock-free:
34+
35+
```nim
36+
import lockfreequeues
37+
38+
# These work - lock-free types
39+
var queue1 = newUnboundedSipsic[64, int]()
40+
var queue2 = newUnboundedSipsic[64, uint64]()
41+
var queue3 = newUnboundedSipsic[64, pointer]()
42+
43+
type NodeObj = object
44+
value: int
45+
var queue4 = newUnboundedSipsic[64, ptr NodeObj]()
46+
47+
# This fails on arc/orc - ref uses spinlocks for refcounting
48+
type Node = ref object
49+
value: int
50+
var queue5 = newUnboundedSipsic[64, Node]() # Compile error!
51+
```
52+
53+
### Why This Matters
54+
55+
On arc/orc memory managers, `ref` types use reference counting. While the queue's CAS operations correctly serialize slot access, reference counting operations on ref types may use spinlock-based atomics on some platforms, potentially introducing subtle issues.
56+
57+
### Allowing Non-Lock-Free Types
58+
59+
If you understand the trade-offs and need to use non-lock-free types:
60+
61+
```bash
62+
nim c -d:allowNonLockFreeQueueItems your_program.nim
63+
```
64+
65+
### Recommended Patterns
66+
67+
For maximum safety and portability:
68+
69+
- **Value types**: Use int, uint64, float, enums, simple objects
70+
- **Pointers**: Use `ptr T` when you need indirection (you manage lifetime)
71+
- **Avoid ref types**: On arc/orc, prefer `ptr T` with manual memory management
72+
- **Test on target platform**: Always verify lock-free status on your deployment target
73+
74+
### Testing Lock-Free Behavior
75+
76+
Include tests with multiple memory managers:
77+
78+
```bash
79+
nim c -r --mm:refc tests/mytest.nim
80+
nim c -r --mm:arc tests/mytest.nim
81+
nim c -r --mm:orc tests/mytest.nim
82+
```
83+
2984
## Examples
3085

3186
Examples are located in the [examples](https://github.com/elijahr/lockfreequeues/tree/master/examples) directory and can be compiled and run with:

docs/slot-ownership-typestates.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Slot Ownership Typestates
2+
3+
## Overview
4+
5+
Slot ownership typestates make data races structurally impossible by tracking slot ownership through the type system. Each state transition enforces critical invariants at compile-time.
6+
7+
## Producer-Side Flow
8+
9+
```
10+
tryClaimSlot()
11+
|
12+
v
13+
SlotClaimed ----[writeItem]----> SlotWritten ----[commitSlot]----> SlotCommitted
14+
| |
15+
v v
16+
SegmentFull [data ready]
17+
```
18+
19+
### States
20+
21+
- **SlotClaimed**: Exclusive write access after winning CAS
22+
- Guarantees: This thread owns the slot index
23+
- Linear type: Can only use once (via `sink` parameter)
24+
25+
- **SlotWritten**: Data written but not visible yet
26+
- Guarantees: Data in slot, but consumers blocked
27+
28+
- **SlotCommitted**: Data visible to consumers
29+
- Guarantees: Consumers can now read
30+
31+
### Transitions
32+
33+
```nim
34+
# Try to claim a slot
35+
let result = tryClaimSlot(segment)
36+
37+
case result.kind:
38+
of ckClaimed:
39+
# Won CAS - have exclusive access
40+
let claimed = result.claimed
41+
42+
# Write data (consumes claimed token)
43+
let written = writeItem(claimed, myData)
44+
45+
# Commit (make visible to consumers)
46+
let committed = commitSlot(written)
47+
48+
of ckSegmentFull:
49+
# Allocate new segment and retry
50+
discard
51+
52+
of ckRetry:
53+
# Lost CAS race, try again
54+
discard
55+
```
56+
57+
## Consumer-Side Flow
58+
59+
```
60+
tryClaimForRead()
61+
|
62+
v
63+
SlotAvailable ----[readItem]----> T (data)
64+
|
65+
v
66+
SlotPending ----[waitForCommit]----> SlotAvailable
67+
```
68+
69+
### States
70+
71+
- **SlotAvailable**: Slot ready to read
72+
- Guarantees: Producer has committed, data is valid
73+
74+
- **SlotPending**: Claimed but not committed (MPSC only)
75+
- Need to wait for producer to commit
76+
77+
### Transitions
78+
79+
```nim
80+
# Try to claim a slot for reading
81+
let result = tryClaimForRead(segment)
82+
83+
case result.kind:
84+
of pkAvailable:
85+
# Data ready - can read immediately
86+
let data = readItem(result.available)
87+
88+
of pkPending:
89+
# Producer hasn't committed yet - wait
90+
let available = waitForCommit(result.pending)
91+
let data = readItem(available)
92+
93+
of pkExhausted:
94+
# Segment empty, advance to next
95+
discard
96+
97+
of pkEmpty:
98+
# Queue empty
99+
discard
100+
```
101+
102+
## Safety Guarantees
103+
104+
| Guarantee | How Enforced |
105+
|-----------|--------------|
106+
| No write without CAS | `writeItem` requires `SlotClaimed`, only from successful CAS |
107+
| No double write | `sink` parameter consumes `SlotClaimed` token |
108+
| No commit without write | `commitSlot` requires `SlotWritten` |
109+
| No read without CAS | `readItem` requires `SlotAvailable`, only from successful CAS |
110+
| No read uncommitted data | `SlotAvailable` only when committed flag set (MPSC) |
111+
112+
## Type System Enforcement
113+
114+
The type system makes these errors impossible:
115+
116+
```nim
117+
# ERROR: Cannot write without claiming
118+
let written = writeItem(segment, data) # No SlotClaimed token!
119+
120+
# ERROR: Cannot reuse claimed token
121+
let claimed = tryClaimSlot(segment).claimed
122+
let written1 = writeItem(claimed, data1)
123+
let written2 = writeItem(claimed, data2) # claimed already consumed!
124+
125+
# ERROR: Cannot commit without writing
126+
let committed = commitSlot(claimed) # Need SlotWritten, not SlotClaimed!
127+
128+
# ERROR: Cannot read without claiming
129+
let data = readItem(segment, 0) # No SlotAvailable token!
130+
```
131+
132+
## Integration with DEBRA Typestates
133+
134+
Slot ownership states compose with DEBRA pin/unpin states:
135+
136+
```nim
137+
type
138+
MPSCPushContext[T; S, MT: static int] = object
139+
pinnedHandle: ThreadHandle[MT]
140+
pinnedEpoch: uint64
141+
queue: ptr UnboundedMupsicBase[S, T, MT]
142+
slotOwnership: SlotClaimed[T, S] # Embedded slot state
143+
```
144+
145+
This ensures:
146+
- Must be pinned to push
147+
- Must claim slot to write
148+
- All invariants enforced together
149+
150+
## MPSC Implementation Example
151+
152+
The unbounded MPSC queue uses the following typestate progression:
153+
154+
### Push Operation States
155+
156+
1. **MPSCPushReady**: Initial state with pinned DEBRA context
157+
2. **MPSCPushSegmentLoaded**: Segment and tail position loaded
158+
3. **MPSCPushSlotClaimed**: Slot claimed via CAS (or SegmentFull/Retry)
159+
4. **MPSCPushItemWritten**: Data written to slot
160+
5. **MPSCPushComplete**: Committed flag set, data visible to consumers
161+
162+
### Example Usage
163+
164+
```nim
165+
import debra
166+
import lockfreequeues/typestates/unbounded_mpsc_push
167+
168+
# Setup
169+
var manager = initDebraManager[4]()
170+
let handle = registerThread(manager)
171+
var queue: UnboundedMupsicBase[64, int, 4]
172+
# ... initialize queue ...
173+
174+
# Push operation
175+
let pinned = unpinned(handle).pin()
176+
let ready = startPush(pinned, addr queue)
177+
let loaded = ready.loadSegment()
178+
179+
let claimResult = loaded.tryClaimSlot()
180+
case claimResult.kind:
181+
of mMPSCPushSlotClaimed:
182+
let claimed = claimResult.mpscpushslotclaimed
183+
let written = claimed.writeItem(42)
184+
let complete = written.markCommitted()
185+
discard complete.extractPinned().unpin()
186+
187+
of mMPSCPushSegmentFull:
188+
# Handle segment allocation
189+
let newSeg = newSegment()
190+
let ready = claimResult.mpscpushsegmentfull.allocateNewSegment(newSeg)
191+
# Retry...
192+
193+
of mMPSCPushReady:
194+
# CAS failed, retry
195+
discard
196+
```
197+
198+
## Performance
199+
200+
Slot ownership typestates are **zero-cost abstractions**:
201+
202+
- All types are compile-time only
203+
- No runtime overhead
204+
- Same assembly as hand-written CAS code
205+
- Just adds compile-time safety
206+
207+
## Future Extensions
208+
209+
Potential enhancements:
210+
211+
- Batch operations: Claim multiple slots at once
212+
- Backpressure: SlotClaimFailed -> wait/retry logic
213+
- Priority: Different claim strategies based on priority
214+
- Monitoring: Track claim success rates

lockfreequeues.nimble

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@ requires "debra >= 0.2.0"
1616

1717
# Tasks
1818
task test, "Runs the test suite":
19-
# C
19+
# C with default MM (orc)
2020
exec "nim c --threads:on -r -f tests/test.nim"
2121

2222
# C++
2323
exec "nim cpp --threads:on -r -f tests/test.nim"
2424

25+
# Test with different memory managers
26+
exec "nim c --mm:arc --threads:on -r -f tests/test.nim"
27+
exec "nim c --mm:refc --threads:on -r -f tests/test.nim"
28+
29+
# Test with lock-free enforcement (ensures no spinlock fallback)
30+
exec "nim c --mm:arc -d:nimEnforceLockFreeAtomics --threads:on -r -f tests/test.nim"
31+
exec "nim c --mm:orc -d:nimEnforceLockFreeAtomics --threads:on -r -f tests/test.nim"
32+
2533
if getEnv("SANITIZE_THREADS") != "no":
2634
# C (with thread sanitization, requires atomicArc for thread-safe refcounting)
2735
exec "nim c --cc:clang --mm:atomicArc --passC:\"-fsanitize=thread\" --passL:\"-fsanitize=thread\" --threads:on -r -f tests/test.nim"
@@ -49,12 +57,19 @@ task benchmarks, "Runs the benchmark suite":
4957

5058

5159
task stresstests, "Runs the stress test suite (multi-threaded)":
52-
# C
60+
# C with default MM (orc)
5361
exec "nim c --path:src --threads:on -r -f stress-tests/stress_test.nim"
5462

5563
# C++
5664
exec "nim cpp --path:src --threads:on -r -f stress-tests/stress_test.nim"
5765

66+
# Test with different memory managers
67+
exec "nim c --mm:arc --path:src --threads:on -r -f stress-tests/stress_test.nim"
68+
exec "nim c --mm:refc --path:src --threads:on -r -f stress-tests/stress_test.nim"
69+
70+
# Test with lock-free enforcement
71+
exec "nim c --mm:arc -d:nimEnforceLockFreeAtomics --path:src --threads:on -r -f stress-tests/stress_test.nim"
72+
5873
if getEnv("SANITIZE_THREADS") != "no":
5974
# C (with thread sanitization)
6075
exec "nim c --cc:clang --mm:atomicArc --path:src --passC:\"-fsanitize=thread\" --passL:\"-fsanitize=thread\" --threads:on -r -f stress-tests/stress_test.nim"

0 commit comments

Comments
 (0)