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.
Files changed (42) hide show
  1. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/CHANGELOG.md +12 -0
  2. py_sharedmemory-4.0.0/PKG-INFO +324 -0
  3. py_sharedmemory-4.0.0/README.md +274 -0
  4. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/benchmark.py +1 -1
  5. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/examples/basic.py +1 -1
  6. py_sharedmemory-4.0.0/examples/numpy_batches.py +56 -0
  7. py_sharedmemory-4.0.0/examples/worker_pool.py +55 -0
  8. py_sharedmemory-4.0.0/py_sharedmemory/__init__.py +29 -0
  9. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/factory.py +14 -1
  10. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/receiver.py +28 -3
  11. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/sender.py +37 -4
  12. py_sharedmemory-4.0.0/py_sharedmemory.egg-info/PKG-INFO +324 -0
  13. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/py_sharedmemory.egg-info/SOURCES.txt +8 -6
  14. py_sharedmemory-4.0.0/py_sharedmemory.egg-info/top_level.txt +1 -0
  15. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/pyproject.toml +16 -11
  16. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/conftest.py +1 -1
  17. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_convert.py +7 -2
  18. py_sharedmemory-4.0.0/tests/test_package.py +44 -0
  19. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_processes.py +1 -1
  20. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_queue.py +5 -5
  21. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_threads.py +1 -1
  22. py_sharedmemory-3.0.1/PKG-INFO +0 -155
  23. py_sharedmemory-3.0.1/README.md +0 -107
  24. py_sharedmemory-3.0.1/memory/__init__.py +0 -8
  25. py_sharedmemory-3.0.1/py_sharedmemory.egg-info/PKG-INFO +0 -155
  26. py_sharedmemory-3.0.1/py_sharedmemory.egg-info/top_level.txt +0 -1
  27. py_sharedmemory-3.0.1/tests/test_package.py +0 -34
  28. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/LICENSE +0 -0
  29. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/MANIFEST.in +0 -0
  30. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/__init__.py +0 -0
  31. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/plot.py +0 -0
  32. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/results/windows-numpy.json +0 -0
  33. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/benchmarks/results/windows-numpy.png +0 -0
  34. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/convert.py +0 -0
  35. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.0.0/py_sharedmemory}/py.typed +0 -0
  36. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
  37. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/py_sharedmemory.egg-info/requires.txt +0 -0
  38. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/setup.cfg +0 -0
  39. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/__init__.py +0 -0
  40. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/helpers.py +0 -0
  41. {py_sharedmemory-3.0.1 → py_sharedmemory-4.0.0}/tests/test_benchmark.py +0 -0
  42. {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
+ [![CI](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
54
+ [![PyPI](https://img.shields.io/pypi/v/py-sharedmemory)](https://pypi.org/project/py-sharedmemory/)
55
+ [![Python](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftobiaswuerth%2Fpython_shared_memory_queue%2Fmain%2Fpyproject.toml&query=%24.project%5B%27requires-python%27%5D&label=python&logo=python&logoColor=white)](https://pypi.org/project/py-sharedmemory/)
56
+ [![Downloads](https://img.shields.io/pypi/dm/py-sharedmemory)](https://pypistats.org/packages/py-sharedmemory)
57
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
58
+ [![License](https://img.shields.io/github/license/tobiaswuerth/python_shared_memory_queue)](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE)
59
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
60
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
61
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](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
+ ![Transfer speed relative to multiprocessing.Queue, from 16 bytes to 10 GB](https://raw.githubusercontent.com/tobiaswuerth/python_shared_memory_queue/main/benchmarks/results/windows-numpy.png)
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
+ [![CI](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/py-sharedmemory)](https://pypi.org/project/py-sharedmemory/)
5
+ [![Python](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ftobiaswuerth%2Fpython_shared_memory_queue%2Fmain%2Fpyproject.toml&query=%24.project%5B%27requires-python%27%5D&label=python&logo=python&logoColor=white)](https://pypi.org/project/py-sharedmemory/)
6
+ [![Downloads](https://img.shields.io/pypi/dm/py-sharedmemory)](https://pypistats.org/packages/py-sharedmemory)
7
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/tobiaswuerth/python_shared_memory_queue/actions/workflows/ci.yml)
8
+ [![License](https://img.shields.io/github/license/tobiaswuerth/python_shared_memory_queue)](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE)
9
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
10
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
11
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](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
+ ![Transfer speed relative to multiprocessing.Queue, from 16 bytes to 10 GB](https://raw.githubusercontent.com/tobiaswuerth/python_shared_memory_queue/main/benchmarks/results/windows-numpy.png)
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 memory import create_shared_memory_pair
14
+ from py_sharedmemory import create_shared_memory_pair
15
15
 
16
16
 
17
17
  def _receive(receiver, count, warmup, control, size):
@@ -2,7 +2,7 @@
2
2
 
3
3
  import multiprocessing as mp
4
4
 
5
- from memory import SharedMemorySender, create_shared_memory_pair
5
+ from py_sharedmemory import SharedMemorySender, create_shared_memory_pair
6
6
 
7
7
 
8
8
  def produce(sender: SharedMemorySender):