py-sharedmemory 3.0.1__tar.gz → 4.0.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.
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/CHANGELOG.md +12 -0
- py_sharedmemory-4.0.0/PKG-INFO +324 -0
- py_sharedmemory-4.0.0/README.md +274 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/benchmark.py +1 -1
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/examples/basic.py +1 -1
- py_sharedmemory-4.0.0/examples/numpy_batches.py +56 -0
- py_sharedmemory-4.0.0/examples/worker_pool.py +55 -0
- py_sharedmemory-4.0.0/py_sharedmemory/__init__.py +29 -0
- {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/factory.py +14 -1
- {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/receiver.py +28 -3
- {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/sender.py +37 -4
- py_sharedmemory-4.0.0/py_sharedmemory.egg-info/PKG-INFO +324 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/py_sharedmemory.egg-info/SOURCES.txt +8 -6
- py_sharedmemory-4.0.0/py_sharedmemory.egg-info/top_level.txt +1 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/pyproject.toml +16 -11
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/conftest.py +1 -1
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_convert.py +7 -2
- py_sharedmemory-4.0.0/tests/test_package.py +44 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_processes.py +1 -1
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_queue.py +5 -5
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_threads.py +1 -1
- py_sharedmemory-3.0.1/PKG-INFO +0 -155
- py_sharedmemory-3.0.1/README.md +0 -107
- py_sharedmemory-3.0.1/memory/__init__.py +0 -8
- py_sharedmemory-3.0.1/py_sharedmemory.egg-info/PKG-INFO +0 -155
- py_sharedmemory-3.0.1/py_sharedmemory.egg-info/top_level.txt +0 -1
- py_sharedmemory-3.0.1/tests/test_package.py +0 -34
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/LICENSE +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/MANIFEST.in +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/__init__.py +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/plot.py +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/results/windows-numpy.json +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/results/windows-numpy.png +0 -0
- {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/convert.py +0 -0
- {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/py.typed +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/py_sharedmemory.egg-info/requires.txt +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/setup.cfg +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/__init__.py +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/helpers.py +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_benchmark.py +0 -0
- {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_objects.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
- **Breaking:** the import package is renamed from `memory` to `py_sharedmemory` to match the
|
|
6
|
+
distribution name. `memory` clashed with the unrelated `memory` package on PyPI and with
|
|
7
|
+
projects' own `memory` modules. Upgrade by replacing `from memory import ...` with
|
|
8
|
+
`from py_sharedmemory import ...`; the API is unchanged and no alias is kept.
|
|
9
|
+
- Rewrite the README as a user guide with an API reference, performance table,
|
|
10
|
+
Docker, macOS, open-file-limit, and troubleshooting notes, and absolute links so
|
|
11
|
+
images render on PyPI.
|
|
12
|
+
- Document every public class and method; add NumPy streaming and worker-pool examples.
|
|
13
|
+
- Add contributing and security guides, issue forms, and a pull request template.
|
|
14
|
+
|
|
3
15
|
## 3.0.1
|
|
4
16
|
|
|
5
17
|
- Fix resource-tracker leaks on Linux and macOS: receivers no longer register blocks they
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-sharedmemory
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: Fast multiprocessing.Queue alternative that moves large Python objects and NumPy arrays through shared memory
|
|
5
|
+
Author: Tobias Würth
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tobiaswuerth/python_shared_memory_queue
|
|
8
|
+
Project-URL: Documentation, https://github.com/tobiaswuerth/python_shared_memory_queue#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
|
|
10
|
+
Project-URL: Changelog, https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Releases, https://github.com/tobiaswuerth/python_shared_memory_queue/releases
|
|
12
|
+
Project-URL: Issues, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
|
|
13
|
+
Keywords: shared-memory,multiprocessing,queue,multiprocessing-queue,ipc,inter-process-communication,numpy,pickle,data-pipeline,machine-learning,reinforcement-learning
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
27
|
+
Classifier: Topic :: Scientific/Engineering
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
30
|
+
Classifier: Typing :: Typed
|
|
31
|
+
Requires-Python: >=3.11
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest>=7.4; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.1; extra == "test"
|
|
37
|
+
Requires-Dist: pytest-timeout>=2.3; extra == "test"
|
|
38
|
+
Requires-Dist: numpy>=1.26; extra == "test"
|
|
39
|
+
Provides-Extra: benchmark
|
|
40
|
+
Requires-Dist: numpy>=1.26; extra == "benchmark"
|
|
41
|
+
Requires-Dist: matplotlib>=3.8; extra == "benchmark"
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: py-sharedmemory[benchmark,test]; extra == "dev"
|
|
44
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
45
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
46
|
+
Requires-Dist: pre-commit>=3.7; extra == "dev"
|
|
47
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
48
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# py-sharedmemory
|
|
52
|
+
|
|
53
|
+
[](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
|
|
54
|
+
[](https://pypi.org/project/py-sharedmemory/)
|
|
55
|
+
[](https://pypi.org/project/py-sharedmemory/)
|
|
56
|
+
[](https://pypistats.org/packages/py-sharedmemory)
|
|
57
|
+
[](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
|
|
58
|
+
[](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE)
|
|
59
|
+
[](https://github.com/astral-sh/ruff)
|
|
60
|
+
[](https://mypy-lang.org/)
|
|
61
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
62
|
+
|
|
63
|
+
**A fast `multiprocessing.Queue` alternative for large Python objects and NumPy arrays.**
|
|
64
|
+
Payloads travel between processes through shared memory instead of pipes; the queues
|
|
65
|
+
underneath carry only a few bytes of metadata per message. Multi-megabyte batches,
|
|
66
|
+
images, and simulation states arrive up to 2× faster, and transfers too large for
|
|
67
|
+
`multiprocessing.Queue` still work.
|
|
68
|
+
|
|
69
|
+
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
70
|
+
`queue.Full`, and `queue.Empty`.
|
|
71
|
+
- **Any picklable object:** NumPy arrays and other buffers use pickle protocol 5
|
|
72
|
+
out-of-band buffers, so they are copied straight into shared memory.
|
|
73
|
+
- **Bounded memory:** a capacity limit applies backpressure, and each shared-memory
|
|
74
|
+
block is freed once its message is received.
|
|
75
|
+
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
76
|
+
- **Portable:** Linux, macOS, and Windows; `spawn`, `fork`, and `forkserver`; Python 3.11–3.15.
|
|
77
|
+
- **Lightweight:** no dependencies, fully typed (PEP 561), and 100% test coverage.
|
|
78
|
+
|
|
79
|
+
Built for **machine-learning data pipelines, reinforcement-learning actors and replay
|
|
80
|
+
buffers, scientific computing, and image or video processing**, where large payloads
|
|
81
|
+
make pipe I/O the bottleneck.
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
pip install py-sharedmemory
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> **Upgrading from 3.x?** The import package is now `py_sharedmemory` (previously
|
|
90
|
+
> `memory`). Replace `from memory import ...` with `from py_sharedmemory import ...`;
|
|
91
|
+
> nothing else changed.
|
|
92
|
+
|
|
93
|
+
## Quick start
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import multiprocessing as mp
|
|
97
|
+
|
|
98
|
+
from py_sharedmemory import create_shared_memory_pair
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def produce(sender):
|
|
102
|
+
with sender:
|
|
103
|
+
sender.put({"step": 42, "frames": bytearray(16_000_000)})
|
|
104
|
+
sender.wait_for_all_ack() # Keep the data alive until it is received
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
ctx = mp.get_context("spawn")
|
|
109
|
+
sender, receiver = create_shared_memory_pair(capacity=2, ctx=ctx)
|
|
110
|
+
worker = ctx.Process(target=produce, args=(sender,))
|
|
111
|
+
worker.start()
|
|
112
|
+
with receiver:
|
|
113
|
+
batch = receiver.get(timeout=30)
|
|
114
|
+
worker.join()
|
|
115
|
+
sender.close()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
More complete, runnable programs:
|
|
119
|
+
|
|
120
|
+
- [`examples/basic.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/basic.py):
|
|
121
|
+
a producer process with timeouts and error handling.
|
|
122
|
+
- [`examples/numpy_batches.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/numpy_batches.py):
|
|
123
|
+
streaming NumPy batches with backpressure and an end-of-stream marker.
|
|
124
|
+
- [`examples/worker_pool.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/worker_pool.py):
|
|
125
|
+
fanning large tasks out to several consumer processes.
|
|
126
|
+
|
|
127
|
+
## When to use it
|
|
128
|
+
|
|
129
|
+
Use py-sharedmemory when individual messages are **around 1 MB or larger**: NumPy
|
|
130
|
+
arrays, image or video frames, RL rollouts, large `bytes`, or big nested objects. It also
|
|
131
|
+
helps where large `multiprocessing.Queue` transfers cause heavy pipe I/O or uneven
|
|
132
|
+
throughput, which is especially noticeable on Windows.
|
|
133
|
+
|
|
134
|
+
Prefer `multiprocessing.Queue` for small messages. Below about 100 kB, creating and
|
|
135
|
+
freeing a shared-memory block (about 0.09 ms per message) costs more than the pipe.
|
|
136
|
+
|
|
137
|
+
To let several processes read and write **the same** array in place, use
|
|
138
|
+
[`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
|
|
139
|
+
directly. py-sharedmemory delivers an independent copy of each message.
|
|
140
|
+
|
|
141
|
+
## Performance
|
|
142
|
+
|
|
143
|
+

|
|
144
|
+
|
|
145
|
+
| Payload per message | `multiprocessing.Queue` | py-sharedmemory | Speedup |
|
|
146
|
+
| --- | ---: | ---: | ---: |
|
|
147
|
+
| 100 kB | 2,315 MiB/s | 797 MiB/s | 0.34× |
|
|
148
|
+
| 1 MB | 1,022 MiB/s | 2,056 MiB/s | **2.01×** |
|
|
149
|
+
| 10 MB | 1,177 MiB/s | 2,387 MiB/s | **2.03×** |
|
|
150
|
+
| 100 MB | 1,034 MiB/s | 1,820 MiB/s | **1.76×** |
|
|
151
|
+
| 1 GB | 855 MiB/s | 1,262 MiB/s | **1.47×** |
|
|
152
|
+
| 10 GB | failed (Windows error 87) | 8.0 s per message | — |
|
|
153
|
+
|
|
154
|
+
Core i9-14900K, 191.8 GiB RAM, Windows 11, Python 3.12.10, NumPy 2.5.3, measured on
|
|
155
|
+
2026-10-08. Contiguous float32 arrays, `spawn`, capacity 2, median of 5 runs excluding
|
|
156
|
+
startup and warmup; up to 1,000 messages or 512 MiB per run, at least one message.
|
|
157
|
+
1 GB = 10⁹ bytes. See the [raw results](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/benchmarks/results/windows-numpy.json).
|
|
158
|
+
These numbers show transfer speed; I/O load and stability depend on the workload.
|
|
159
|
+
|
|
160
|
+
## How it works
|
|
161
|
+
|
|
162
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data,
|
|
163
|
+
stay out-of-band, and everything is copied into one new shared-memory block.
|
|
164
|
+
2. Only the block's name and buffer sizes travel through a `multiprocessing.Queue`.
|
|
165
|
+
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
166
|
+
sends an acknowledgement back.
|
|
167
|
+
4. A background thread in the producer frees the block when the acknowledgement
|
|
168
|
+
arrives. `capacity` limits how many blocks can be outstanding at once.
|
|
169
|
+
|
|
170
|
+
Received objects never point into shared memory, so they stay valid after the block is
|
|
171
|
+
freed and can be modified freely.
|
|
172
|
+
|
|
173
|
+
## Usage guide
|
|
174
|
+
|
|
175
|
+
### Passing endpoints to processes
|
|
176
|
+
|
|
177
|
+
Create the pair in the parent process and pass the endpoints to workers as `Process`
|
|
178
|
+
arguments, the same way as a `multiprocessing.Queue`. Use the same context for the pair
|
|
179
|
+
and the processes. A sender can be passed to another process only **before** its first
|
|
180
|
+
`put`; after that it belongs to the process that used it.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
ctx = mp.get_context("spawn") # "fork" and "forkserver" also work
|
|
184
|
+
sender, receiver = create_shared_memory_pair(capacity=4, ctx=ctx)
|
|
185
|
+
producer = ctx.Process(target=produce, args=(sender,))
|
|
186
|
+
consumer = ctx.Process(target=consume, args=(receiver,))
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Capacity and backpressure
|
|
190
|
+
|
|
191
|
+
`capacity` counts messages that were sent but not yet received and freed. When it is
|
|
192
|
+
reached, `put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is
|
|
193
|
+
at most about `capacity × message size`. `capacity=0` or `None` means unbounded; prefer
|
|
194
|
+
a limit, because unread messages also hold open files on Linux and macOS (see the
|
|
195
|
+
platform notes below).
|
|
196
|
+
|
|
197
|
+
### Timeouts and non-blocking calls
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from queue import Empty, Full
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
sender.put(batch, timeout=5) # Wait up to 5 s for capacity
|
|
204
|
+
except Full:
|
|
205
|
+
...
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
item = receiver.get_nowait() # Same as get(block=False)
|
|
209
|
+
except Empty:
|
|
210
|
+
...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Multiple consumers and threads
|
|
214
|
+
|
|
215
|
+
Each message is delivered to exactly one consumer. Several consumer processes can share
|
|
216
|
+
one receiver, as in [`examples/worker_pool.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/worker_pool.py),
|
|
217
|
+
and both endpoints are thread-safe. Only one process may produce per pair; for several
|
|
218
|
+
producer processes, create one pair per producer.
|
|
219
|
+
|
|
220
|
+
### Shutting down cleanly
|
|
221
|
+
|
|
222
|
+
The producer owns every message until it is acknowledged, and `close()` frees unread
|
|
223
|
+
messages. Before the producer closes or exits:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
with sender:
|
|
227
|
+
for item in items:
|
|
228
|
+
sender.put(item)
|
|
229
|
+
if not sender.wait_for_all_ack(timeout=30):
|
|
230
|
+
raise TimeoutError("Consumer did not receive everything")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
When both endpoints live in one process, drain before closing either of them. A sentinel
|
|
234
|
+
such as `None` is a simple way to tell consumers that the stream has ended.
|
|
235
|
+
|
|
236
|
+
### NumPy arrays
|
|
237
|
+
|
|
238
|
+
Arrays keep their dtype, shape, and writability. Read-only arrays stay read-only, and
|
|
239
|
+
writable ones arrive as writable local copies. C- and Fortran-contiguous arrays keep
|
|
240
|
+
their layout and are copied straight into shared memory without an intermediate pickle
|
|
241
|
+
copy; other views, such as strided slices, arrive as contiguous copies.
|
|
242
|
+
|
|
243
|
+
### Errors
|
|
244
|
+
|
|
245
|
+
| Exception | Raised when |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `queue.Full` | `put` found no capacity within `timeout` or with `block=False`. |
|
|
248
|
+
| `queue.Empty` | `get` found no message within `timeout` or with `block=False`. |
|
|
249
|
+
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
250
|
+
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
251
|
+
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
|
|
252
|
+
|
|
253
|
+
## API reference
|
|
254
|
+
|
|
255
|
+
### `create_shared_memory_pair(capacity=0, *, ctx=None)`
|
|
256
|
+
|
|
257
|
+
Returns a connected `(SharedMemorySender, SharedMemoryReceiver)`. `capacity` limits
|
|
258
|
+
outstanding messages (`0` or `None` is unbounded). `ctx` is the multiprocessing context
|
|
259
|
+
for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
260
|
+
|
|
261
|
+
### `SharedMemorySender`
|
|
262
|
+
|
|
263
|
+
| Member | Description |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
|
|
266
|
+
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
267
|
+
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
|
|
268
|
+
| `close()` | Free unread messages and release resources. Runs automatically at normal process exit. |
|
|
269
|
+
| `closed` | Whether the sender is closed. |
|
|
270
|
+
| `with sender:` | Closes the sender on exit. |
|
|
271
|
+
|
|
272
|
+
### `SharedMemoryReceiver`
|
|
273
|
+
|
|
274
|
+
| Member | Description |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| `get(block=True, timeout=None)` | Return the next object as an independent local copy. |
|
|
277
|
+
| `get_nowait()` | `get(block=False)`. |
|
|
278
|
+
| `close()` | Flush acknowledgements and close this process's handles. |
|
|
279
|
+
| `closed` | Whether the receiver is closed. |
|
|
280
|
+
| `with receiver:` | Closes the receiver on exit. |
|
|
281
|
+
|
|
282
|
+
## Platform notes and troubleshooting
|
|
283
|
+
|
|
284
|
+
**Docker and Kubernetes:** on Linux, shared memory lives in `/dev/shm`, which containers
|
|
285
|
+
limit to 64 MB by default. Raise it to at least `capacity × largest message`, for example
|
|
286
|
+
`docker run --shm-size=2g` or `shm_size: 2gb` in Docker Compose. In Kubernetes, mount an
|
|
287
|
+
`emptyDir` volume with `medium: Memory` at `/dev/shm`. Otherwise the producer can fail
|
|
288
|
+
with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
|
|
289
|
+
|
|
290
|
+
**Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
|
|
291
|
+
|
|
292
|
+
**macOS:** fully supported and tested on Apple Silicon. The default start method is
|
|
293
|
+
`spawn`, so guard the entry point with `if __name__ == "__main__":`.
|
|
294
|
+
|
|
295
|
+
**Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
|
|
296
|
+
open in the producer until it is received. macOS allows 256 per process by default and
|
|
297
|
+
Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
|
|
298
|
+
files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
|
|
299
|
+
raise the limit with `ulimit -n`. The sender stays usable after this error.
|
|
300
|
+
|
|
301
|
+
**Windows:** shared memory is backed by the page file, and each block disappears once
|
|
302
|
+
the last handle closes, so nothing can leak after a crash.
|
|
303
|
+
|
|
304
|
+
**"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
|
|
305
|
+
3.0.1. Upgrade if you see this warning with the `fork` start method.
|
|
306
|
+
|
|
307
|
+
**`RuntimeError: A shared-memory pair supports one producing process`:** a second
|
|
308
|
+
process called `put`. Create a separate pair for each producer process.
|
|
309
|
+
|
|
310
|
+
**`Can't pickle local object` or similar:** messages must be picklable. Lambdas, local
|
|
311
|
+
functions, open files, and locks cannot be sent.
|
|
312
|
+
|
|
313
|
+
## Contributing
|
|
314
|
+
|
|
315
|
+
Bug reports, ideas, and pull requests are welcome. See
|
|
316
|
+
[CONTRIBUTING.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CONTRIBUTING.md)
|
|
317
|
+
for the development setup, tests, and release process, and the
|
|
318
|
+
[changelog](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CHANGELOG.md)
|
|
319
|
+
for version history. Report security issues privately as described in
|
|
320
|
+
[SECURITY.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/SECURITY.md).
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
[MIT](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE) © Tobias Würth
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# py-sharedmemory
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/py-sharedmemory/)
|
|
5
|
+
[](https://pypi.org/project/py-sharedmemory/)
|
|
6
|
+
[](https://pypistats.org/packages/py-sharedmemory)
|
|
7
|
+
[](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
|
|
8
|
+
[](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE)
|
|
9
|
+
[](https://github.com/astral-sh/ruff)
|
|
10
|
+
[](https://mypy-lang.org/)
|
|
11
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
12
|
+
|
|
13
|
+
**A fast `multiprocessing.Queue` alternative for large Python objects and NumPy arrays.**
|
|
14
|
+
Payloads travel between processes through shared memory instead of pipes; the queues
|
|
15
|
+
underneath carry only a few bytes of metadata per message. Multi-megabyte batches,
|
|
16
|
+
images, and simulation states arrive up to 2× faster, and transfers too large for
|
|
17
|
+
`multiprocessing.Queue` still work.
|
|
18
|
+
|
|
19
|
+
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
20
|
+
`queue.Full`, and `queue.Empty`.
|
|
21
|
+
- **Any picklable object:** NumPy arrays and other buffers use pickle protocol 5
|
|
22
|
+
out-of-band buffers, so they are copied straight into shared memory.
|
|
23
|
+
- **Bounded memory:** a capacity limit applies backpressure, and each shared-memory
|
|
24
|
+
block is freed once its message is received.
|
|
25
|
+
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
26
|
+
- **Portable:** Linux, macOS, and Windows; `spawn`, `fork`, and `forkserver`; Python 3.11–3.15.
|
|
27
|
+
- **Lightweight:** no dependencies, fully typed (PEP 561), and 100% test coverage.
|
|
28
|
+
|
|
29
|
+
Built for **machine-learning data pipelines, reinforcement-learning actors and replay
|
|
30
|
+
buffers, scientific computing, and image or video processing**, where large payloads
|
|
31
|
+
make pipe I/O the bottleneck.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install py-sharedmemory
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **Upgrading from 3.x?** The import package is now `py_sharedmemory` (previously
|
|
40
|
+
> `memory`). Replace `from memory import ...` with `from py_sharedmemory import ...`;
|
|
41
|
+
> nothing else changed.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import multiprocessing as mp
|
|
47
|
+
|
|
48
|
+
from py_sharedmemory import create_shared_memory_pair
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def produce(sender):
|
|
52
|
+
with sender:
|
|
53
|
+
sender.put({"step": 42, "frames": bytearray(16_000_000)})
|
|
54
|
+
sender.wait_for_all_ack() # Keep the data alive until it is received
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
ctx = mp.get_context("spawn")
|
|
59
|
+
sender, receiver = create_shared_memory_pair(capacity=2, ctx=ctx)
|
|
60
|
+
worker = ctx.Process(target=produce, args=(sender,))
|
|
61
|
+
worker.start()
|
|
62
|
+
with receiver:
|
|
63
|
+
batch = receiver.get(timeout=30)
|
|
64
|
+
worker.join()
|
|
65
|
+
sender.close()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
More complete, runnable programs:
|
|
69
|
+
|
|
70
|
+
- [`examples/basic.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/basic.py):
|
|
71
|
+
a producer process with timeouts and error handling.
|
|
72
|
+
- [`examples/numpy_batches.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/numpy_batches.py):
|
|
73
|
+
streaming NumPy batches with backpressure and an end-of-stream marker.
|
|
74
|
+
- [`examples/worker_pool.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/worker_pool.py):
|
|
75
|
+
fanning large tasks out to several consumer processes.
|
|
76
|
+
|
|
77
|
+
## When to use it
|
|
78
|
+
|
|
79
|
+
Use py-sharedmemory when individual messages are **around 1 MB or larger**: NumPy
|
|
80
|
+
arrays, image or video frames, RL rollouts, large `bytes`, or big nested objects. It also
|
|
81
|
+
helps where large `multiprocessing.Queue` transfers cause heavy pipe I/O or uneven
|
|
82
|
+
throughput, which is especially noticeable on Windows.
|
|
83
|
+
|
|
84
|
+
Prefer `multiprocessing.Queue` for small messages. Below about 100 kB, creating and
|
|
85
|
+
freeing a shared-memory block (about 0.09 ms per message) costs more than the pipe.
|
|
86
|
+
|
|
87
|
+
To let several processes read and write **the same** array in place, use
|
|
88
|
+
[`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
|
|
89
|
+
directly. py-sharedmemory delivers an independent copy of each message.
|
|
90
|
+
|
|
91
|
+
## Performance
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+
| Payload per message | `multiprocessing.Queue` | py-sharedmemory | Speedup |
|
|
96
|
+
| --- | ---: | ---: | ---: |
|
|
97
|
+
| 100 kB | 2,315 MiB/s | 797 MiB/s | 0.34× |
|
|
98
|
+
| 1 MB | 1,022 MiB/s | 2,056 MiB/s | **2.01×** |
|
|
99
|
+
| 10 MB | 1,177 MiB/s | 2,387 MiB/s | **2.03×** |
|
|
100
|
+
| 100 MB | 1,034 MiB/s | 1,820 MiB/s | **1.76×** |
|
|
101
|
+
| 1 GB | 855 MiB/s | 1,262 MiB/s | **1.47×** |
|
|
102
|
+
| 10 GB | failed (Windows error 87) | 8.0 s per message | — |
|
|
103
|
+
|
|
104
|
+
Core i9-14900K, 191.8 GiB RAM, Windows 11, Python 3.12.10, NumPy 2.5.3, measured on
|
|
105
|
+
2026-10-08. Contiguous float32 arrays, `spawn`, capacity 2, median of 5 runs excluding
|
|
106
|
+
startup and warmup; up to 1,000 messages or 512 MiB per run, at least one message.
|
|
107
|
+
1 GB = 10⁹ bytes. See the [raw results](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/benchmarks/results/windows-numpy.json).
|
|
108
|
+
These numbers show transfer speed; I/O load and stability depend on the workload.
|
|
109
|
+
|
|
110
|
+
## How it works
|
|
111
|
+
|
|
112
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data,
|
|
113
|
+
stay out-of-band, and everything is copied into one new shared-memory block.
|
|
114
|
+
2. Only the block's name and buffer sizes travel through a `multiprocessing.Queue`.
|
|
115
|
+
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
116
|
+
sends an acknowledgement back.
|
|
117
|
+
4. A background thread in the producer frees the block when the acknowledgement
|
|
118
|
+
arrives. `capacity` limits how many blocks can be outstanding at once.
|
|
119
|
+
|
|
120
|
+
Received objects never point into shared memory, so they stay valid after the block is
|
|
121
|
+
freed and can be modified freely.
|
|
122
|
+
|
|
123
|
+
## Usage guide
|
|
124
|
+
|
|
125
|
+
### Passing endpoints to processes
|
|
126
|
+
|
|
127
|
+
Create the pair in the parent process and pass the endpoints to workers as `Process`
|
|
128
|
+
arguments, the same way as a `multiprocessing.Queue`. Use the same context for the pair
|
|
129
|
+
and the processes. A sender can be passed to another process only **before** its first
|
|
130
|
+
`put`; after that it belongs to the process that used it.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
ctx = mp.get_context("spawn") # "fork" and "forkserver" also work
|
|
134
|
+
sender, receiver = create_shared_memory_pair(capacity=4, ctx=ctx)
|
|
135
|
+
producer = ctx.Process(target=produce, args=(sender,))
|
|
136
|
+
consumer = ctx.Process(target=consume, args=(receiver,))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Capacity and backpressure
|
|
140
|
+
|
|
141
|
+
`capacity` counts messages that were sent but not yet received and freed. When it is
|
|
142
|
+
reached, `put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is
|
|
143
|
+
at most about `capacity × message size`. `capacity=0` or `None` means unbounded; prefer
|
|
144
|
+
a limit, because unread messages also hold open files on Linux and macOS (see the
|
|
145
|
+
platform notes below).
|
|
146
|
+
|
|
147
|
+
### Timeouts and non-blocking calls
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from queue import Empty, Full
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
sender.put(batch, timeout=5) # Wait up to 5 s for capacity
|
|
154
|
+
except Full:
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
item = receiver.get_nowait() # Same as get(block=False)
|
|
159
|
+
except Empty:
|
|
160
|
+
...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Multiple consumers and threads
|
|
164
|
+
|
|
165
|
+
Each message is delivered to exactly one consumer. Several consumer processes can share
|
|
166
|
+
one receiver, as in [`examples/worker_pool.py`](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/examples/worker_pool.py),
|
|
167
|
+
and both endpoints are thread-safe. Only one process may produce per pair; for several
|
|
168
|
+
producer processes, create one pair per producer.
|
|
169
|
+
|
|
170
|
+
### Shutting down cleanly
|
|
171
|
+
|
|
172
|
+
The producer owns every message until it is acknowledged, and `close()` frees unread
|
|
173
|
+
messages. Before the producer closes or exits:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
with sender:
|
|
177
|
+
for item in items:
|
|
178
|
+
sender.put(item)
|
|
179
|
+
if not sender.wait_for_all_ack(timeout=30):
|
|
180
|
+
raise TimeoutError("Consumer did not receive everything")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
When both endpoints live in one process, drain before closing either of them. A sentinel
|
|
184
|
+
such as `None` is a simple way to tell consumers that the stream has ended.
|
|
185
|
+
|
|
186
|
+
### NumPy arrays
|
|
187
|
+
|
|
188
|
+
Arrays keep their dtype, shape, and writability. Read-only arrays stay read-only, and
|
|
189
|
+
writable ones arrive as writable local copies. C- and Fortran-contiguous arrays keep
|
|
190
|
+
their layout and are copied straight into shared memory without an intermediate pickle
|
|
191
|
+
copy; other views, such as strided slices, arrive as contiguous copies.
|
|
192
|
+
|
|
193
|
+
### Errors
|
|
194
|
+
|
|
195
|
+
| Exception | Raised when |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `queue.Full` | `put` found no capacity within `timeout` or with `block=False`. |
|
|
198
|
+
| `queue.Empty` | `get` found no message within `timeout` or with `block=False`. |
|
|
199
|
+
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
200
|
+
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
201
|
+
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
|
|
202
|
+
|
|
203
|
+
## API reference
|
|
204
|
+
|
|
205
|
+
### `create_shared_memory_pair(capacity=0, *, ctx=None)`
|
|
206
|
+
|
|
207
|
+
Returns a connected `(SharedMemorySender, SharedMemoryReceiver)`. `capacity` limits
|
|
208
|
+
outstanding messages (`0` or `None` is unbounded). `ctx` is the multiprocessing context
|
|
209
|
+
for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
210
|
+
|
|
211
|
+
### `SharedMemorySender`
|
|
212
|
+
|
|
213
|
+
| Member | Description |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
|
|
216
|
+
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
217
|
+
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
|
|
218
|
+
| `close()` | Free unread messages and release resources. Runs automatically at normal process exit. |
|
|
219
|
+
| `closed` | Whether the sender is closed. |
|
|
220
|
+
| `with sender:` | Closes the sender on exit. |
|
|
221
|
+
|
|
222
|
+
### `SharedMemoryReceiver`
|
|
223
|
+
|
|
224
|
+
| Member | Description |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `get(block=True, timeout=None)` | Return the next object as an independent local copy. |
|
|
227
|
+
| `get_nowait()` | `get(block=False)`. |
|
|
228
|
+
| `close()` | Flush acknowledgements and close this process's handles. |
|
|
229
|
+
| `closed` | Whether the receiver is closed. |
|
|
230
|
+
| `with receiver:` | Closes the receiver on exit. |
|
|
231
|
+
|
|
232
|
+
## Platform notes and troubleshooting
|
|
233
|
+
|
|
234
|
+
**Docker and Kubernetes:** on Linux, shared memory lives in `/dev/shm`, which containers
|
|
235
|
+
limit to 64 MB by default. Raise it to at least `capacity × largest message`, for example
|
|
236
|
+
`docker run --shm-size=2g` or `shm_size: 2gb` in Docker Compose. In Kubernetes, mount an
|
|
237
|
+
`emptyDir` volume with `medium: Memory` at `/dev/shm`. Otherwise the producer can fail
|
|
238
|
+
with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
|
|
239
|
+
|
|
240
|
+
**Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
|
|
241
|
+
|
|
242
|
+
**macOS:** fully supported and tested on Apple Silicon. The default start method is
|
|
243
|
+
`spawn`, so guard the entry point with `if __name__ == "__main__":`.
|
|
244
|
+
|
|
245
|
+
**Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
|
|
246
|
+
open in the producer until it is received. macOS allows 256 per process by default and
|
|
247
|
+
Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
|
|
248
|
+
files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
|
|
249
|
+
raise the limit with `ulimit -n`. The sender stays usable after this error.
|
|
250
|
+
|
|
251
|
+
**Windows:** shared memory is backed by the page file, and each block disappears once
|
|
252
|
+
the last handle closes, so nothing can leak after a crash.
|
|
253
|
+
|
|
254
|
+
**"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
|
|
255
|
+
3.0.1. Upgrade if you see this warning with the `fork` start method.
|
|
256
|
+
|
|
257
|
+
**`RuntimeError: A shared-memory pair supports one producing process`:** a second
|
|
258
|
+
process called `put`. Create a separate pair for each producer process.
|
|
259
|
+
|
|
260
|
+
**`Can't pickle local object` or similar:** messages must be picklable. Lambdas, local
|
|
261
|
+
functions, open files, and locks cannot be sent.
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Bug reports, ideas, and pull requests are welcome. See
|
|
266
|
+
[CONTRIBUTING.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CONTRIBUTING.md)
|
|
267
|
+
for the development setup, tests, and release process, and the
|
|
268
|
+
[changelog](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CHANGELOG.md)
|
|
269
|
+
for version history. Report security issues privately as described in
|
|
270
|
+
[SECURITY.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/SECURITY.md).
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
[MIT](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE) © Tobias Würth
|
|
@@ -11,7 +11,7 @@ from contextlib import redirect_stderr
|
|
|
11
11
|
from datetime import UTC, datetime
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from
|
|
14
|
+
from py_sharedmemory import create_shared_memory_pair
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _receive(receiver, count, warmup, control, size):
|