slick-queue-py 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/API_DIFFERENCES.md +37 -2
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/CHANGELOG.md +54 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/PKG-INFO +84 -2
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/README.md +83 -1
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/slick_queue_py.egg-info/SOURCES.txt +3 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/slick_queue_py.py +578 -213
- slick_queue_py-2.4.0/tests/cpp_spsc_tester.cpp +136 -0
- slick_queue_py-2.4.0/tests/test_spsc.py +715 -0
- slick_queue_py-2.4.0/tests/test_spsc_interop.py +138 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/FUNDING.yml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/ci.yml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/publish-conda.yml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/publish-pypi.yml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/publish-release.yml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.github/workflows/release.yml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.gitignore +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/.pypirc.template +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/BUILDING.md +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/CMakeLists.txt +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/LICENSE +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/MANIFEST.in +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/atomic_ops.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/atomic_ops_ext.cpp +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/conda.recipe/meta.yaml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/pyproject.toml +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/requirements.txt +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/setup.cfg +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/setup.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cleanup_shm.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_consumer.cpp +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_multi_producer.cpp +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_producer.cpp +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_read_last_tester.cpp +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/cpp_work_stealing_consumer.cpp +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/run_test.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_atomic_cursor.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_atomic_ops.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_interop.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_items_per_slot.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_local_mode.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_loss_count.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_modern_format.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_multi_producer.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_queue.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_read_last.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_read_last_interop.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_reset_detection.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_segment_lifetime.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_traits.py +0 -0
- {slick_queue_py-2.3.0 → slick_queue_py-2.4.0}/tests/test_wrap_invariants.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# C++ / Python API Differences
|
|
2
2
|
|
|
3
|
-
This document explains the intentional API differences between the C++ `slick::queue<T>` and Python `SlickQueue` implementations.
|
|
3
|
+
This document explains the intentional API differences between the C++ `slick::queue<T>` and Python `SlickQueue` implementations, and between C++ `slick::spsc_queue<T>` and Python `SpscQueue` (section 8).
|
|
4
4
|
|
|
5
|
-
Both track the same version line; this document describes slick-queue v2.
|
|
5
|
+
Both track the same version line; this document describes slick-queue v2.4.0.
|
|
6
6
|
|
|
7
7
|
## Core Compatibility
|
|
8
8
|
|
|
@@ -267,6 +267,39 @@ the process exits.
|
|
|
267
267
|
|
|
268
268
|
---
|
|
269
269
|
|
|
270
|
+
### 8. SpscQueue: Held Slot vs. Copy
|
|
271
|
+
|
|
272
|
+
**C++:**
|
|
273
|
+
```cpp
|
|
274
|
+
slick::spsc_queue<uint64_t> q("ticks");
|
|
275
|
+
if (uint64_t* v = q.read()) { /* slot held until the next read(), read_last() or release() */ }
|
|
276
|
+
uint64_t dropped = 0;
|
|
277
|
+
uint64_t* last = q.read_last(dropped);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Python:**
|
|
281
|
+
```python
|
|
282
|
+
q = SpscQueue(name='ticks', element_size=8)
|
|
283
|
+
data = q.read() # bytes copy; the slot is already back with the producer
|
|
284
|
+
data, dropped = q.read_last()
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Rationale:** Python copies the element anyway, so once `read()` returns there is nothing
|
|
288
|
+
left to hold, and it hands the slot back at once. A Python consumer therefore gets the full
|
|
289
|
+
`size` as capacity, even at size 1, and has no `release()`. A C++ consumer that exits while
|
|
290
|
+
holding a slot still has that element delivered again to whoever attaches next, Python
|
|
291
|
+
included.
|
|
292
|
+
|
|
293
|
+
| C++ | Python |
|
|
294
|
+
|-----|--------|
|
|
295
|
+
| `try_reserve()` returns `invalid_index` | Same, as `SpscQueue.invalid_index` |
|
|
296
|
+
| `read()` returns `T*`, `nullptr` when empty | `bytes`, `None` when empty |
|
|
297
|
+
| `read_last(dropped)` with an out-parameter | `read_last()` returns `(data, dropped)` |
|
|
298
|
+
| `release()` | Not needed: nothing is held |
|
|
299
|
+
| `spsc_queue(name)` adopts `size`, and `T` gives the element size | `SpscQueue(name=..., element_size=...)` adopts `size`; `element_size` is always required |
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
270
303
|
## Interoperability Examples
|
|
271
304
|
|
|
272
305
|
### Example 1: C++ Producer → Python Consumer
|
|
@@ -363,5 +396,7 @@ int main() {
|
|
|
363
396
|
| Misconfigured traits | Compile error (concept / `static_assert`) | `TypeError` / `RuntimeError` at runtime | No compile step |
|
|
364
397
|
| Default loss detection | Off in Release | Always on | +0.2% here, and the only overrun signal a caller has |
|
|
365
398
|
| Layout marker | `'SLQ1'` / `'SLQ0'` | Identical, and validated against C++ peers | Shared contract |
|
|
399
|
+
| SPSC `read()` | `T*` held until the next read | `bytes` copy, slot freed at once | Python copies anyway |
|
|
400
|
+
| SPSC `read_last()` dropped count | Out-parameter | Second item of the returned tuple | Python has no pass-by-reference |
|
|
366
401
|
|
|
367
402
|
**Bottom Line:** The Python API is designed to be Pythonic while maintaining **100% binary compatibility** with C++. All atomic operations and memory layouts are identical, enabling seamless C++/Python interoperability.
|
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Changelogs
|
|
2
2
|
|
|
3
|
+
## [v2.4.0] - 2026-11-09
|
|
4
|
+
|
|
5
|
+
Tracks C++ slick-queue v2.4.0, which added `slick::spsc_queue`. The `SlickQueue`
|
|
6
|
+
shared-memory layout is unchanged, so 2.3.0 peers in either language interoperate
|
|
7
|
+
with it as before.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `SpscQueue`, a port of C++ `slick::spsc_queue<T>`: a lossless single-producer
|
|
12
|
+
single-consumer ring in local or shared memory. `try_reserve(n)` returns
|
|
13
|
+
`SpscQueue.invalid_index` when the ring is full and `reserve(n)` waits until the
|
|
14
|
+
consumer frees room. `publish()` never moves the tail backwards, so publishing an
|
|
15
|
+
earlier reservation after a later one is a no-op.
|
|
16
|
+
- `read()` returns the next element as `bytes`. `read_last()` returns
|
|
17
|
+
`(data, dropped)`, the newest element and how many unread ones it skipped, from
|
|
18
|
+
one load of the producer's tail however large the backlog.
|
|
19
|
+
- Both reads copy the element and hand its slot back before they return. C++ holds
|
|
20
|
+
the slot until the consumer's next call, so a Python consumer gets the full `size`
|
|
21
|
+
as capacity, nothing it read is delivered again after it reattaches, and there is
|
|
22
|
+
no `release()`.
|
|
23
|
+
- `SpscQueueTraits` with one setting, `enable_cpu_relax`, which yields the GIL while
|
|
24
|
+
`reserve()` waits. `q.traits` is a read-only snapshot, as for `SlickQueue`.
|
|
25
|
+
- The C++ shared-memory layout: a 384-byte header with the tail at offset 128 and the
|
|
26
|
+
head at 256, then the data array. Size, element size, the `'SLS0'` marker and init
|
|
27
|
+
state keep the `SlickQueue` offsets. Both cursors live in the segment, so a
|
|
28
|
+
reattached peer resumes where the previous one stopped.
|
|
29
|
+
- A creator that claims a segment left uninitialized, whose own creator died before
|
|
30
|
+
claiming it, checks the mapping holds its geometry first. If it doesn't, it raises
|
|
31
|
+
and hands the claim back, instead of writing a header for a ring the mapping can't
|
|
32
|
+
hold. Every attacher checks the mapping size too.
|
|
33
|
+
- Using an `SpscQueue` after `close()` raises `RuntimeError("queue is closed")`.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- `SlickQueue` attaching to an `SpscQueue` segment now raises "holds a
|
|
38
|
+
slick::spsc_queue" instead of the generic missing-marker error, matching C++, and
|
|
39
|
+
`SpscQueue` names a `SlickQueue` segment the same way.
|
|
40
|
+
- Name handling, opening and closing the segment, `unlink()`, `remove_shm()`,
|
|
41
|
+
`remove()` and the context manager moved into a base class both queues share.
|
|
42
|
+
`SlickQueue` behaves as before.
|
|
43
|
+
- `close()` still never raises, but no longer prints the exception it swallows.
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
- `tests/test_spsc.py` covers the local and shared-memory API: wraps, a full ring,
|
|
48
|
+
out-of-order publish, element sizes other than 8, `read_last()` accounting under a
|
|
49
|
+
concurrent producer, reset, reattach, a too-small leftover segment, use after
|
|
50
|
+
`close()` and the cross-type marker checks.
|
|
51
|
+
- `tests/test_spsc_interop.py` runs against C++ through the new `cpp_spsc_tester`:
|
|
52
|
+
data in both directions, attaching to a C++-created segment, `read_last()` from
|
|
53
|
+
each side, and each language rejecting the other queue type.
|
|
54
|
+
|
|
55
|
+
Checked against C++ slick-queue v2.4.0 with the interop tests on Windows.
|
|
56
|
+
|
|
3
57
|
## [v2.3.0] - 2026-10-17
|
|
4
58
|
|
|
5
59
|
Tracks C++ slick-queue v2.3.0: no queue removes its shared-memory segment on its
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slick-queue-py
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Lock-free MPMC queue with C++ interoperability via shared memory
|
|
5
5
|
Home-page: https://github.com/SlickQuant/slick-queue-py
|
|
6
6
|
Author: Slick Quant
|
|
@@ -36,7 +36,7 @@ Dynamic: requires-python
|
|
|
36
36
|
|
|
37
37
|
# slick-queue-py
|
|
38
38
|
|
|
39
|
-
Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
|
|
39
|
+
Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue, plus a lossless single-producer single-consumer (SPSC) queue, with C++ interoperability through shared memory.
|
|
40
40
|
|
|
41
41
|
This is the Python binding for the [SlickQueue C++ library](https://github.com/SlickQuant/slick-queue). The Python implementation maintains exact binary compatibility with the C++ version, enabling seamless interprocess communication between Python and C++ applications.
|
|
42
42
|
|
|
@@ -50,6 +50,8 @@ This is the Python binding for the [SlickQueue C++ library](https://github.com/S
|
|
|
50
50
|
- **Local Memory Mode**: In-process queue using local memory (no shared memory overhead)
|
|
51
51
|
- **Shared Memory Mode**: Inter-process queue for interprocess communication
|
|
52
52
|
- **Lock-Free Multi-Producer Multi-Consumer**: True MPMC support using atomic operations
|
|
53
|
+
- **Lossless SPSC Queue**: `SpscQueue` ports C++ `slick::spsc_queue`. It never overwrites an
|
|
54
|
+
unread element and reports a full ring to the producer instead. See [SpscQueue](#spscqueue)
|
|
53
55
|
- **C++/Python Interoperability**: Python and C++ processes can share the same queue
|
|
54
56
|
- **Cross-Platform**: Windows and Linux/macOS support (x86-64)
|
|
55
57
|
- **Memory Layout Compatible**: Exact binary compatibility with C++ `slick::queue<T>`
|
|
@@ -642,6 +644,51 @@ sends the tracker nothing at all: `track=False` on Python 3.13+, and on older PO
|
|
|
642
644
|
`SharedMemory` subclass that maps the segment without registering it, so there is no
|
|
643
645
|
register/unregister pair to race between threads or between processes sharing one tracker.
|
|
644
646
|
|
|
647
|
+
### SpscQueue
|
|
648
|
+
|
|
649
|
+
A lossless single-producer single-consumer ring, compatible with C++ `slick::spsc_queue<T>`. One
|
|
650
|
+
thread or process produces and one consumes. Unlike `SlickQueue` it never overwrites an unread
|
|
651
|
+
element: `try_reserve()` reports a full ring instead.
|
|
652
|
+
|
|
653
|
+
```python
|
|
654
|
+
import struct
|
|
655
|
+
from slick_queue_py import SpscQueue
|
|
656
|
+
|
|
657
|
+
q = SpscQueue(name='ticks', size=1024, element_size=8) # create, or open with the same geometry
|
|
658
|
+
peer = SpscQueue(name='ticks', element_size=8) # attach, adopting the segment's size
|
|
659
|
+
local = SpscQueue(size=1024, element_size=8) # local memory
|
|
660
|
+
|
|
661
|
+
# Producer
|
|
662
|
+
i = q.try_reserve() # SpscQueue.invalid_index when full; reserve() waits instead
|
|
663
|
+
if i != SpscQueue.invalid_index:
|
|
664
|
+
q[i][:8] = struct.pack('<Q', 42)
|
|
665
|
+
q.publish(i)
|
|
666
|
+
|
|
667
|
+
# Consumer
|
|
668
|
+
data = peer.read() # next element as bytes, or None
|
|
669
|
+
data, dropped = peer.read_last() # newest element, and how many unread ones it skipped
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
- `reserve(n)` and `try_reserve(n)` reserve `n` separate elements. Write element `k` through
|
|
673
|
+
`q[i + k]`, because a batch may wrap around the end of the ring. Publish in reservation order.
|
|
674
|
+
Publishing a later batch first also exposes the earlier one, and publishing the earlier one
|
|
675
|
+
afterwards is a no-op.
|
|
676
|
+
- `read()` and `read_last()` copy the element and hand its slot back before they return. C++ holds
|
|
677
|
+
the slot until the consumer's next call, so here the full `size` is always usable and there is no
|
|
678
|
+
`release()`.
|
|
679
|
+
- `read_last()` loads the producer's tail and moves the read cursor there, so its cost doesn't grow
|
|
680
|
+
with the backlog. It returns `(None, 0)` if nothing was published since the last read. Note
|
|
681
|
+
that the second item counts skipped elements, where `SlickQueue.read_last()` returns a size.
|
|
682
|
+
- Both cursors live in the segment, and a peer that reattaches resumes where the previous one
|
|
683
|
+
stopped. Each object caches its own side's cursor, so keep one live object per side. After
|
|
684
|
+
another process has produced or consumed, attach a fresh object rather than reusing an old one.
|
|
685
|
+
- `reset()` empties the queue. It isn't thread-safe, and a peer in another process must reattach.
|
|
686
|
+
- `close()`, `unlink()`, `remove_shm()`, `SpscQueue.remove(name)`, `get_shm_name()` and the
|
|
687
|
+
context manager behave as they do for `SlickQueue`. Using the queue after `close()` raises
|
|
688
|
+
`RuntimeError("queue is closed")`.
|
|
689
|
+
- `traits=` takes an `SpscQueueTraits` subclass. Its one setting is `enable_cpu_relax` (default
|
|
690
|
+
`True`), which yields the GIL while `reserve()` waits. `q.traits` is a read-only snapshot.
|
|
691
|
+
|
|
645
692
|
### AtomicCursor
|
|
646
693
|
|
|
647
694
|
The `AtomicCursor` class enables multi-consumer work-stealing patterns by providing an atomic read cursor that multiple consumers can coordinate through. Works in both local mode (multi-threading) and shared memory mode (multi-process).
|
|
@@ -817,6 +864,34 @@ Bits 2-3 of the nibble are reserved and must be 0; a marker that sets one is rej
|
|
|
817
864
|
newer than this build understands. Segments created before slick-queue v1.4.0 carry no
|
|
818
865
|
marker at all and are rejected at attach time.
|
|
819
866
|
|
|
867
|
+
### SpscQueue layout
|
|
868
|
+
|
|
869
|
+
`SpscQueue` matches C++ `slick::spsc_queue<T>`. Each cursor sits on its own 128-byte line on every
|
|
870
|
+
platform:
|
|
871
|
+
|
|
872
|
+
```
|
|
873
|
+
Offset | Size | Content
|
|
874
|
+
-------|---------------|------------------
|
|
875
|
+
0 | 8 bytes | reserved (0)
|
|
876
|
+
8 | 4 bytes | uint32_t size (queue capacity)
|
|
877
|
+
12 | 4 bytes | uint32_t element_size
|
|
878
|
+
16 | 8 bytes | reserved (0)
|
|
879
|
+
24 | 4 bytes | uint32_t header_magic - 'SLS0' (0x534C5330)
|
|
880
|
+
28 | 20 bytes | reserved (0)
|
|
881
|
+
48 | 4 bytes | uint32_t init_state (atomic)
|
|
882
|
+
52 | 76 bytes | padding (to 128)
|
|
883
|
+
128 | 8 bytes | uint64_t tail (atomic), written by the producer
|
|
884
|
+
136 | 120 bytes | padding
|
|
885
|
+
256 | 8 bytes | uint64_t head (atomic), written by the consumer
|
|
886
|
+
264 | 120 bytes | padding (to 384)
|
|
887
|
+
384 | elem*size | data array
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
`size`, `element_size`, the marker and `init_state` sit at the same offsets as in a `SlickQueue`
|
|
891
|
+
segment. Each queue type therefore turns the other's segment away at attach time, with *"holds a
|
|
892
|
+
slick::spsc_queue"* or *"holds a slick::queue"*, instead of timing out. The marker's low nibble is
|
|
893
|
+
reserved for future layout features and must be 0. The C++ side requires `alignof(T) <= 128`.
|
|
894
|
+
|
|
820
895
|
## Platform Support
|
|
821
896
|
|
|
822
897
|
### Fully Supported (Lock-Free)
|
|
@@ -959,6 +1034,9 @@ python tests/test_wrap_invariants.py
|
|
|
959
1034
|
|
|
960
1035
|
# Segment lifetime: unlink(), remove_shm(), remove(), process exit
|
|
961
1036
|
python tests/test_segment_lifetime.py
|
|
1037
|
+
|
|
1038
|
+
# SpscQueue, local and shared memory
|
|
1039
|
+
python tests/test_spsc.py
|
|
962
1040
|
```
|
|
963
1041
|
|
|
964
1042
|
Or run everything with pytest:
|
|
@@ -987,6 +1065,9 @@ python tests/test_interop.py --test cpp_producer_python_consumer
|
|
|
987
1065
|
python tests/test_interop.py --test multi_producer_interop
|
|
988
1066
|
python tests/test_interop.py --test stress_interop
|
|
989
1067
|
python tests/test_interop.py --test cpp_shm_creation
|
|
1068
|
+
|
|
1069
|
+
# SpscQueue against C++ slick::spsc_queue (uses cpp_spsc_tester)
|
|
1070
|
+
python tests/test_spsc_interop.py
|
|
990
1071
|
```
|
|
991
1072
|
|
|
992
1073
|
The interop tests verify:
|
|
@@ -1052,6 +1133,7 @@ queue so consumers keep up, and use `loss_count()` to detect when they have not.
|
|
|
1052
1133
|
| read(atomic cursor) multi-consumer | ✅ | ✅ |
|
|
1053
1134
|
| Feature traits | Template parameter | `traits=` argument |
|
|
1054
1135
|
| Shared layout marker | `'SLQ1'` / `'SLQ0'` | Same, and validated against C++ peers |
|
|
1136
|
+
| Lossless SPSC queue | `slick::spsc_queue<T>` | `SpscQueue`, same `'SLS0'` layout |
|
|
1055
1137
|
|
|
1056
1138
|
## Contributing
|
|
1057
1139
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# slick-queue-py
|
|
2
2
|
|
|
3
|
-
Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue with C++ interoperability through shared memory.
|
|
3
|
+
Python implementation of SlickQueue - a lock-free multi-producer multi-consumer (MPMC) queue, plus a lossless single-producer single-consumer (SPSC) queue, with C++ interoperability through shared memory.
|
|
4
4
|
|
|
5
5
|
This is the Python binding for the [SlickQueue C++ library](https://github.com/SlickQuant/slick-queue). The Python implementation maintains exact binary compatibility with the C++ version, enabling seamless interprocess communication between Python and C++ applications.
|
|
6
6
|
|
|
@@ -14,6 +14,8 @@ This is the Python binding for the [SlickQueue C++ library](https://github.com/S
|
|
|
14
14
|
- **Local Memory Mode**: In-process queue using local memory (no shared memory overhead)
|
|
15
15
|
- **Shared Memory Mode**: Inter-process queue for interprocess communication
|
|
16
16
|
- **Lock-Free Multi-Producer Multi-Consumer**: True MPMC support using atomic operations
|
|
17
|
+
- **Lossless SPSC Queue**: `SpscQueue` ports C++ `slick::spsc_queue`. It never overwrites an
|
|
18
|
+
unread element and reports a full ring to the producer instead. See [SpscQueue](#spscqueue)
|
|
17
19
|
- **C++/Python Interoperability**: Python and C++ processes can share the same queue
|
|
18
20
|
- **Cross-Platform**: Windows and Linux/macOS support (x86-64)
|
|
19
21
|
- **Memory Layout Compatible**: Exact binary compatibility with C++ `slick::queue<T>`
|
|
@@ -606,6 +608,51 @@ sends the tracker nothing at all: `track=False` on Python 3.13+, and on older PO
|
|
|
606
608
|
`SharedMemory` subclass that maps the segment without registering it, so there is no
|
|
607
609
|
register/unregister pair to race between threads or between processes sharing one tracker.
|
|
608
610
|
|
|
611
|
+
### SpscQueue
|
|
612
|
+
|
|
613
|
+
A lossless single-producer single-consumer ring, compatible with C++ `slick::spsc_queue<T>`. One
|
|
614
|
+
thread or process produces and one consumes. Unlike `SlickQueue` it never overwrites an unread
|
|
615
|
+
element: `try_reserve()` reports a full ring instead.
|
|
616
|
+
|
|
617
|
+
```python
|
|
618
|
+
import struct
|
|
619
|
+
from slick_queue_py import SpscQueue
|
|
620
|
+
|
|
621
|
+
q = SpscQueue(name='ticks', size=1024, element_size=8) # create, or open with the same geometry
|
|
622
|
+
peer = SpscQueue(name='ticks', element_size=8) # attach, adopting the segment's size
|
|
623
|
+
local = SpscQueue(size=1024, element_size=8) # local memory
|
|
624
|
+
|
|
625
|
+
# Producer
|
|
626
|
+
i = q.try_reserve() # SpscQueue.invalid_index when full; reserve() waits instead
|
|
627
|
+
if i != SpscQueue.invalid_index:
|
|
628
|
+
q[i][:8] = struct.pack('<Q', 42)
|
|
629
|
+
q.publish(i)
|
|
630
|
+
|
|
631
|
+
# Consumer
|
|
632
|
+
data = peer.read() # next element as bytes, or None
|
|
633
|
+
data, dropped = peer.read_last() # newest element, and how many unread ones it skipped
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
- `reserve(n)` and `try_reserve(n)` reserve `n` separate elements. Write element `k` through
|
|
637
|
+
`q[i + k]`, because a batch may wrap around the end of the ring. Publish in reservation order.
|
|
638
|
+
Publishing a later batch first also exposes the earlier one, and publishing the earlier one
|
|
639
|
+
afterwards is a no-op.
|
|
640
|
+
- `read()` and `read_last()` copy the element and hand its slot back before they return. C++ holds
|
|
641
|
+
the slot until the consumer's next call, so here the full `size` is always usable and there is no
|
|
642
|
+
`release()`.
|
|
643
|
+
- `read_last()` loads the producer's tail and moves the read cursor there, so its cost doesn't grow
|
|
644
|
+
with the backlog. It returns `(None, 0)` if nothing was published since the last read. Note
|
|
645
|
+
that the second item counts skipped elements, where `SlickQueue.read_last()` returns a size.
|
|
646
|
+
- Both cursors live in the segment, and a peer that reattaches resumes where the previous one
|
|
647
|
+
stopped. Each object caches its own side's cursor, so keep one live object per side. After
|
|
648
|
+
another process has produced or consumed, attach a fresh object rather than reusing an old one.
|
|
649
|
+
- `reset()` empties the queue. It isn't thread-safe, and a peer in another process must reattach.
|
|
650
|
+
- `close()`, `unlink()`, `remove_shm()`, `SpscQueue.remove(name)`, `get_shm_name()` and the
|
|
651
|
+
context manager behave as they do for `SlickQueue`. Using the queue after `close()` raises
|
|
652
|
+
`RuntimeError("queue is closed")`.
|
|
653
|
+
- `traits=` takes an `SpscQueueTraits` subclass. Its one setting is `enable_cpu_relax` (default
|
|
654
|
+
`True`), which yields the GIL while `reserve()` waits. `q.traits` is a read-only snapshot.
|
|
655
|
+
|
|
609
656
|
### AtomicCursor
|
|
610
657
|
|
|
611
658
|
The `AtomicCursor` class enables multi-consumer work-stealing patterns by providing an atomic read cursor that multiple consumers can coordinate through. Works in both local mode (multi-threading) and shared memory mode (multi-process).
|
|
@@ -781,6 +828,34 @@ Bits 2-3 of the nibble are reserved and must be 0; a marker that sets one is rej
|
|
|
781
828
|
newer than this build understands. Segments created before slick-queue v1.4.0 carry no
|
|
782
829
|
marker at all and are rejected at attach time.
|
|
783
830
|
|
|
831
|
+
### SpscQueue layout
|
|
832
|
+
|
|
833
|
+
`SpscQueue` matches C++ `slick::spsc_queue<T>`. Each cursor sits on its own 128-byte line on every
|
|
834
|
+
platform:
|
|
835
|
+
|
|
836
|
+
```
|
|
837
|
+
Offset | Size | Content
|
|
838
|
+
-------|---------------|------------------
|
|
839
|
+
0 | 8 bytes | reserved (0)
|
|
840
|
+
8 | 4 bytes | uint32_t size (queue capacity)
|
|
841
|
+
12 | 4 bytes | uint32_t element_size
|
|
842
|
+
16 | 8 bytes | reserved (0)
|
|
843
|
+
24 | 4 bytes | uint32_t header_magic - 'SLS0' (0x534C5330)
|
|
844
|
+
28 | 20 bytes | reserved (0)
|
|
845
|
+
48 | 4 bytes | uint32_t init_state (atomic)
|
|
846
|
+
52 | 76 bytes | padding (to 128)
|
|
847
|
+
128 | 8 bytes | uint64_t tail (atomic), written by the producer
|
|
848
|
+
136 | 120 bytes | padding
|
|
849
|
+
256 | 8 bytes | uint64_t head (atomic), written by the consumer
|
|
850
|
+
264 | 120 bytes | padding (to 384)
|
|
851
|
+
384 | elem*size | data array
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
`size`, `element_size`, the marker and `init_state` sit at the same offsets as in a `SlickQueue`
|
|
855
|
+
segment. Each queue type therefore turns the other's segment away at attach time, with *"holds a
|
|
856
|
+
slick::spsc_queue"* or *"holds a slick::queue"*, instead of timing out. The marker's low nibble is
|
|
857
|
+
reserved for future layout features and must be 0. The C++ side requires `alignof(T) <= 128`.
|
|
858
|
+
|
|
784
859
|
## Platform Support
|
|
785
860
|
|
|
786
861
|
### Fully Supported (Lock-Free)
|
|
@@ -923,6 +998,9 @@ python tests/test_wrap_invariants.py
|
|
|
923
998
|
|
|
924
999
|
# Segment lifetime: unlink(), remove_shm(), remove(), process exit
|
|
925
1000
|
python tests/test_segment_lifetime.py
|
|
1001
|
+
|
|
1002
|
+
# SpscQueue, local and shared memory
|
|
1003
|
+
python tests/test_spsc.py
|
|
926
1004
|
```
|
|
927
1005
|
|
|
928
1006
|
Or run everything with pytest:
|
|
@@ -951,6 +1029,9 @@ python tests/test_interop.py --test cpp_producer_python_consumer
|
|
|
951
1029
|
python tests/test_interop.py --test multi_producer_interop
|
|
952
1030
|
python tests/test_interop.py --test stress_interop
|
|
953
1031
|
python tests/test_interop.py --test cpp_shm_creation
|
|
1032
|
+
|
|
1033
|
+
# SpscQueue against C++ slick::spsc_queue (uses cpp_spsc_tester)
|
|
1034
|
+
python tests/test_spsc_interop.py
|
|
954
1035
|
```
|
|
955
1036
|
|
|
956
1037
|
The interop tests verify:
|
|
@@ -1016,6 +1097,7 @@ queue so consumers keep up, and use `loss_count()` to detect when they have not.
|
|
|
1016
1097
|
| read(atomic cursor) multi-consumer | ✅ | ✅ |
|
|
1017
1098
|
| Feature traits | Template parameter | `traits=` argument |
|
|
1018
1099
|
| Shared layout marker | `'SLQ1'` / `'SLQ0'` | Same, and validated against C++ peers |
|
|
1100
|
+
| Lossless SPSC queue | `slick::spsc_queue<T>` | `SpscQueue`, same `'SLS0'` layout |
|
|
1019
1101
|
|
|
1020
1102
|
## Contributing
|
|
1021
1103
|
|
|
@@ -25,6 +25,7 @@ tests/cpp_consumer.cpp
|
|
|
25
25
|
tests/cpp_multi_producer.cpp
|
|
26
26
|
tests/cpp_producer.cpp
|
|
27
27
|
tests/cpp_read_last_tester.cpp
|
|
28
|
+
tests/cpp_spsc_tester.cpp
|
|
28
29
|
tests/cpp_work_stealing_consumer.cpp
|
|
29
30
|
tests/run_test.py
|
|
30
31
|
tests/test_atomic_cursor.py
|
|
@@ -40,5 +41,7 @@ tests/test_read_last.py
|
|
|
40
41
|
tests/test_read_last_interop.py
|
|
41
42
|
tests/test_reset_detection.py
|
|
42
43
|
tests/test_segment_lifetime.py
|
|
44
|
+
tests/test_spsc.py
|
|
45
|
+
tests/test_spsc_interop.py
|
|
43
46
|
tests/test_traits.py
|
|
44
47
|
tests/test_wrap_invariants.py
|