py-sharedmemory 3.0.1__tar.gz → 4.1.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 (43) hide show
  1. py_sharedmemory-4.1.0/CHANGELOG.md +39 -0
  2. py_sharedmemory-4.1.0/PKG-INFO +330 -0
  3. py_sharedmemory-4.1.0/README.md +280 -0
  4. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/benchmarks/benchmark.py +1 -1
  5. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/examples/basic.py +1 -1
  6. py_sharedmemory-4.1.0/examples/numpy_batches.py +56 -0
  7. py_sharedmemory-4.1.0/examples/worker_pool.py +55 -0
  8. py_sharedmemory-4.1.0/py_sharedmemory/__init__.py +29 -0
  9. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.1.0/py_sharedmemory}/factory.py +14 -1
  10. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.1.0/py_sharedmemory}/receiver.py +28 -3
  11. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.1.0/py_sharedmemory}/sender.py +68 -6
  12. py_sharedmemory-4.1.0/py_sharedmemory.egg-info/PKG-INFO +330 -0
  13. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/SOURCES.txt +8 -6
  14. py_sharedmemory-4.1.0/py_sharedmemory.egg-info/top_level.txt +1 -0
  15. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/pyproject.toml +16 -11
  16. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/conftest.py +11 -1
  17. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/helpers.py +37 -5
  18. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/test_convert.py +7 -2
  19. py_sharedmemory-4.1.0/tests/test_package.py +44 -0
  20. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/test_processes.py +49 -1
  21. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/test_queue.py +69 -5
  22. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/test_threads.py +1 -1
  23. py_sharedmemory-3.0.1/CHANGELOG.md +0 -20
  24. py_sharedmemory-3.0.1/PKG-INFO +0 -155
  25. py_sharedmemory-3.0.1/README.md +0 -107
  26. py_sharedmemory-3.0.1/memory/__init__.py +0 -8
  27. py_sharedmemory-3.0.1/py_sharedmemory.egg-info/PKG-INFO +0 -155
  28. py_sharedmemory-3.0.1/py_sharedmemory.egg-info/top_level.txt +0 -1
  29. py_sharedmemory-3.0.1/tests/test_package.py +0 -34
  30. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/LICENSE +0 -0
  31. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/MANIFEST.in +0 -0
  32. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/benchmarks/__init__.py +0 -0
  33. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/benchmarks/plot.py +0 -0
  34. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/benchmarks/results/windows-numpy.json +0 -0
  35. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/benchmarks/results/windows-numpy.png +0 -0
  36. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.1.0/py_sharedmemory}/convert.py +0 -0
  37. {py_sharedmemory-3.0.1/memory → py_sharedmemory-4.1.0/py_sharedmemory}/py.typed +0 -0
  38. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
  39. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/py_sharedmemory.egg-info/requires.txt +0 -0
  40. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/setup.cfg +0 -0
  41. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/__init__.py +0 -0
  42. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/test_benchmark.py +0 -0
  43. {py_sharedmemory-3.0.1 → py_sharedmemory-4.1.0}/tests/test_objects.py +0 -0
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ ## 4.1.0
4
+
5
+ - Warn with a `RuntimeWarning` when leaving a `with sender:` block or exiting the
6
+ producing process discards unacknowledged messages, so a forgotten
7
+ `wait_for_all_ack()` no longer loses data silently. Explicit `close()` calls and
8
+ exits caused by exceptions stay silent, and closing still never blocks.
9
+
10
+ ## 4.0.0
11
+
12
+ - **Breaking:** the import package is renamed from `memory` to `py_sharedmemory` to match the
13
+ distribution name. `memory` clashed with the unrelated `memory` package on PyPI and with
14
+ projects' own `memory` modules. Upgrade by replacing `from memory import ...` with
15
+ `from py_sharedmemory import ...`; the API is unchanged and no alias is kept.
16
+ - Rewrite the README as a user guide with an API reference, performance table,
17
+ Docker, macOS, open-file-limit, and troubleshooting notes, and absolute links so
18
+ images render on PyPI.
19
+ - Document every public class and method; add NumPy streaming and worker-pool examples.
20
+ - Add contributing and security guides, issue forms, and a pull request template.
21
+
22
+ ## 3.0.1
23
+
24
+ - Fix resource-tracker leaks on Linux and macOS: receivers no longer register blocks they
25
+ attach to, so `fork` workers no longer accumulate tracker entries or print
26
+ "leaked shared_memory" warnings at exit.
27
+ - Ship type hints (`py.typed`), checked with strict mypy; expose `memory.__version__`.
28
+ - Support and test Python 3.15; test Linux, macOS, and Windows for every Python version.
29
+ - Require 100% branch coverage; add pre-commit hooks, Dependabot, and a trusted-publishing
30
+ release workflow.
31
+
32
+ ## 3.0.0
33
+
34
+ - Python 3.11+, standard-library-only runtime.
35
+ - Fix acknowledgement races, capacity leaks, failed-put recovery, and process cleanup.
36
+ - Preserve NumPy mutability; remove redundant copies and retained pickle storage.
37
+ - Simplify core ownership and sender flow; use immutable metadata and recover from partial startup.
38
+ - Add explicit close, context managers, context selection, and timed acknowledgement waits.
39
+ - Replace legacy tests and benchmarks; add platform CI and recorded machine results.
@@ -0,0 +1,330 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-sharedmemory
3
+ Version: 4.1.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
+ Closing never blocks. If a `with sender:` block or the producing process ends normally
234
+ while messages are still unacknowledged, they are discarded and the sender emits a
235
+ `RuntimeWarning`, so forgetting to drain does not lose data silently. An explicit
236
+ `close()` or an exception leaving the `with` block discards without a warning.
237
+
238
+ When both endpoints live in one process, drain before closing either of them. A sentinel
239
+ such as `None` is a simple way to tell consumers that the stream has ended.
240
+
241
+ ### NumPy arrays
242
+
243
+ Arrays keep their dtype, shape, and writability. Read-only arrays stay read-only, and
244
+ writable ones arrive as writable local copies. C- and Fortran-contiguous arrays keep
245
+ their layout and are copied straight into shared memory without an intermediate pickle
246
+ copy; other views, such as strided slices, arrive as contiguous copies.
247
+
248
+ ### Errors
249
+
250
+ | Exception | Raised when |
251
+ | --- | --- |
252
+ | `queue.Full` | `put` found no capacity within `timeout` or with `block=False`. |
253
+ | `queue.Empty` | `get` found no message within `timeout` or with `block=False`. |
254
+ | `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
255
+ | `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
256
+ | Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
257
+ | `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
258
+
259
+ ## API reference
260
+
261
+ ### `create_shared_memory_pair(capacity=0, *, ctx=None)`
262
+
263
+ Returns a connected `(SharedMemorySender, SharedMemoryReceiver)`. `capacity` limits
264
+ outstanding messages (`0` or `None` is unbounded). `ctx` is the multiprocessing context
265
+ for the underlying queues; it defaults to `multiprocessing.get_context()`.
266
+
267
+ ### `SharedMemorySender`
268
+
269
+ | Member | Description |
270
+ | --- | --- |
271
+ | `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
272
+ | `put_nowait(obj)` | `put(obj, block=False)`. |
273
+ | `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
274
+ | `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
275
+ | `closed` | Whether the sender is closed. |
276
+ | `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
277
+
278
+ ### `SharedMemoryReceiver`
279
+
280
+ | Member | Description |
281
+ | --- | --- |
282
+ | `get(block=True, timeout=None)` | Return the next object as an independent local copy. |
283
+ | `get_nowait()` | `get(block=False)`. |
284
+ | `close()` | Flush acknowledgements and close this process's handles. |
285
+ | `closed` | Whether the receiver is closed. |
286
+ | `with receiver:` | Closes the receiver on exit. |
287
+
288
+ ## Platform notes and troubleshooting
289
+
290
+ **Docker and Kubernetes:** on Linux, shared memory lives in `/dev/shm`, which containers
291
+ limit to 64 MB by default. Raise it to at least `capacity × largest message`, for example
292
+ `docker run --shm-size=2g` or `shm_size: 2gb` in Docker Compose. In Kubernetes, mount an
293
+ `emptyDir` volume with `medium: Memory` at `/dev/shm`. Otherwise the producer can fail
294
+ with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
295
+
296
+ **Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
297
+
298
+ **macOS:** fully supported and tested on Apple Silicon. The default start method is
299
+ `spawn`, so guard the entry point with `if __name__ == "__main__":`.
300
+
301
+ **Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
302
+ open in the producer until it is received. macOS allows 256 per process by default and
303
+ Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
304
+ files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
305
+ raise the limit with `ulimit -n`. The sender stays usable after this error.
306
+
307
+ **Windows:** shared memory is backed by the page file, and each block disappears once
308
+ the last handle closes, so nothing can leak after a crash.
309
+
310
+ **"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
311
+ 3.0.1. Upgrade if you see this warning with the `fork` start method.
312
+
313
+ **`RuntimeError: A shared-memory pair supports one producing process`:** a second
314
+ process called `put`. Create a separate pair for each producer process.
315
+
316
+ **`Can't pickle local object` or similar:** messages must be picklable. Lambdas, local
317
+ functions, open files, and locks cannot be sent.
318
+
319
+ ## Contributing
320
+
321
+ Bug reports, ideas, and pull requests are welcome. See
322
+ [CONTRIBUTING.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CONTRIBUTING.md)
323
+ for the development setup, tests, and release process, and the
324
+ [changelog](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CHANGELOG.md)
325
+ for version history. Report security issues privately as described in
326
+ [SECURITY.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/SECURITY.md).
327
+
328
+ ## License
329
+
330
+ [MIT](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/LICENSE) © Tobias Würth
@@ -0,0 +1,280 @@
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
+ Closing never blocks. If a `with sender:` block or the producing process ends normally
184
+ while messages are still unacknowledged, they are discarded and the sender emits a
185
+ `RuntimeWarning`, so forgetting to drain does not lose data silently. An explicit
186
+ `close()` or an exception leaving the `with` block discards without a warning.
187
+
188
+ When both endpoints live in one process, drain before closing either of them. A sentinel
189
+ such as `None` is a simple way to tell consumers that the stream has ended.
190
+
191
+ ### NumPy arrays
192
+
193
+ Arrays keep their dtype, shape, and writability. Read-only arrays stay read-only, and
194
+ writable ones arrive as writable local copies. C- and Fortran-contiguous arrays keep
195
+ their layout and are copied straight into shared memory without an intermediate pickle
196
+ copy; other views, such as strided slices, arrive as contiguous copies.
197
+
198
+ ### Errors
199
+
200
+ | Exception | Raised when |
201
+ | --- | --- |
202
+ | `queue.Full` | `put` found no capacity within `timeout` or with `block=False`. |
203
+ | `queue.Empty` | `get` found no message within `timeout` or with `block=False`. |
204
+ | `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
205
+ | `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
206
+ | Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still freed. |
207
+ | `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
208
+
209
+ ## API reference
210
+
211
+ ### `create_shared_memory_pair(capacity=0, *, ctx=None)`
212
+
213
+ Returns a connected `(SharedMemorySender, SharedMemoryReceiver)`. `capacity` limits
214
+ outstanding messages (`0` or `None` is unbounded). `ctx` is the multiprocessing context
215
+ for the underlying queues; it defaults to `multiprocessing.get_context()`.
216
+
217
+ ### `SharedMemorySender`
218
+
219
+ | Member | Description |
220
+ | --- | --- |
221
+ | `put(obj, block=True, timeout=None)` | Copy `obj` into a new shared-memory block and send it. |
222
+ | `put_nowait(obj)` | `put(obj, block=False)`. |
223
+ | `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and freed; `False` on timeout. |
224
+ | `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
225
+ | `closed` | Whether the sender is closed. |
226
+ | `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
227
+
228
+ ### `SharedMemoryReceiver`
229
+
230
+ | Member | Description |
231
+ | --- | --- |
232
+ | `get(block=True, timeout=None)` | Return the next object as an independent local copy. |
233
+ | `get_nowait()` | `get(block=False)`. |
234
+ | `close()` | Flush acknowledgements and close this process's handles. |
235
+ | `closed` | Whether the receiver is closed. |
236
+ | `with receiver:` | Closes the receiver on exit. |
237
+
238
+ ## Platform notes and troubleshooting
239
+
240
+ **Docker and Kubernetes:** on Linux, shared memory lives in `/dev/shm`, which containers
241
+ limit to 64 MB by default. Raise it to at least `capacity × largest message`, for example
242
+ `docker run --shm-size=2g` or `shm_size: 2gb` in Docker Compose. In Kubernetes, mount an
243
+ `emptyDir` volume with `medium: Memory` at `/dev/shm`. Otherwise the producer can fail
244
+ with `OSError: [Errno 28] No space left on device` or crash with `SIGBUS`.
245
+
246
+ **Linux:** outside containers, `/dev/shm` usually defaults to half of physical RAM.
247
+
248
+ **macOS:** fully supported and tested on Apple Silicon. The default start method is
249
+ `spawn`, so guard the entry point with `if __name__ == "__main__":`.
250
+
251
+ **Open-file limit (Linux and macOS):** each unread message keeps two file descriptors
252
+ open in the producer until it is received. macOS allows 256 per process by default and
253
+ Linux usually 1,024, so an unbounded pair fails with `OSError: [Errno 24] Too many open
254
+ files` after roughly 120 or 500 unread messages. Set a `capacity` well below that, or
255
+ raise the limit with `ulimit -n`. The sender stays usable after this error.
256
+
257
+ **Windows:** shared memory is backed by the page file, and each block disappears once
258
+ the last handle closes, so nothing can leak after a crash.
259
+
260
+ **"resource_tracker: There appear to be N leaked shared_memory objects":** fixed in
261
+ 3.0.1. Upgrade if you see this warning with the `fork` start method.
262
+
263
+ **`RuntimeError: A shared-memory pair supports one producing process`:** a second
264
+ process called `put`. Create a separate pair for each producer process.
265
+
266
+ **`Can't pickle local object` or similar:** messages must be picklable. Lambdas, local
267
+ functions, open files, and locks cannot be sent.
268
+
269
+ ## Contributing
270
+
271
+ Bug reports, ideas, and pull requests are welcome. See
272
+ [CONTRIBUTING.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CONTRIBUTING.md)
273
+ for the development setup, tests, and release process, and the
274
+ [changelog](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/CHANGELOG.md)
275
+ for version history. Report security issues privately as described in
276
+ [SECURITY.md](https://github.com/tobiaswuerth/python_shared_memory_queue/blob/main/SECURITY.md).
277
+
278
+ ## License
279
+
280
+ [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):