py-sharedmemory 4.1.1__tar.gz → 4.2.1__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-4.1.1 → py_sharedmemory-4.2.1}/CHANGELOG.md +32 -0
- {py_sharedmemory-4.1.1/py_sharedmemory.egg-info → py_sharedmemory-4.2.1}/PKG-INFO +40 -24
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/README.md +39 -23
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory/__init__.py +1 -1
- py_sharedmemory-4.2.1/py_sharedmemory/convert.py +155 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory/receiver.py +10 -8
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory/sender.py +107 -31
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1/py_sharedmemory.egg-info}/PKG-INFO +40 -24
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/SOURCES.txt +1 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/conftest.py +2 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/helpers.py +11 -1
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_convert.py +145 -8
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_queue.py +108 -33
- py_sharedmemory-4.2.1/tests/test_recycling.py +253 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_threads.py +4 -4
- py_sharedmemory-4.1.1/py_sharedmemory/convert.py +0 -110
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/LICENSE +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/MANIFEST.in +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/benchmarks/__init__.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/benchmarks/benchmark.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/benchmarks/plot.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/benchmarks/results/windows-numpy.json +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/benchmarks/results/windows-numpy.png +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/examples/basic.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/examples/numpy_batches.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/examples/worker_pool.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory/factory.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory/py.typed +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/requires.txt +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/top_level.txt +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/pyproject.toml +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/setup.cfg +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/__init__.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_benchmark.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_objects.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_package.py +0 -0
- {py_sharedmemory-4.1.1 → py_sharedmemory-4.2.1}/tests/test_processes.py +0 -0
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.1
|
|
4
|
+
|
|
5
|
+
- `put` copies large `bytes` and `bytearray` objects (64 KiB and up) straight into shared
|
|
6
|
+
memory, wherever they appear in a message. It used to build the complete pickle in
|
|
7
|
+
memory first: an extra copy of the payload that briefly needed up to 1.5 times its size
|
|
8
|
+
(48 MiB for a 32 MiB `bytearray`). Strided NumPy arrays, which NumPy pickles as `bytes`,
|
|
9
|
+
skip that copy too. The message format is unchanged.
|
|
10
|
+
- Resizing a `bytearray` while its message is being pickled, from a `__reduce__` method,
|
|
11
|
+
now raises `BufferError` instead of risking a corrupted message.
|
|
12
|
+
|
|
13
|
+
## 4.2.0
|
|
14
|
+
|
|
15
|
+
- Reuse received shared-memory blocks instead of freeing them. Writing into fresh pages
|
|
16
|
+
cost far more than the copy itself, so large `put` calls are now much faster. A block
|
|
17
|
+
is reused for a message that fills at least half of it, or for any message that fits a
|
|
18
|
+
block of up to 1 MiB. The sender keeps at most 8 blocks and 256 MiB for reuse, never
|
|
19
|
+
more blocks than the capacity allows, and frees them on `close()`, so peak memory is
|
|
20
|
+
unchanged. On Linux and macOS at most 8 unread blocks stay mapped (two file
|
|
21
|
+
descriptors each).
|
|
22
|
+
- Acknowledgements carry a per-message sequence number, so an unknown or repeated
|
|
23
|
+
acknowledgement can never release a reused block that holds a newer message.
|
|
24
|
+
- Fix receivers hanging forever in `close()` or at process exit after reading many
|
|
25
|
+
messages that a closed sender had already freed: such messages are no longer
|
|
26
|
+
acknowledged, so the acknowledgement pipe can no longer fill up.
|
|
27
|
+
- Fix a Windows race where a message freed while a receiver attached to it raised a
|
|
28
|
+
confusing `UnpicklingError`; it now raises the usual "freed before it was received"
|
|
29
|
+
`FileNotFoundError`.
|
|
30
|
+
- Received NumPy arrays are always aligned. Arrays that followed an odd-sized buffer in the
|
|
31
|
+
same message used to arrive unaligned, which slows NumPy and breaks libraries that need
|
|
32
|
+
aligned memory. Each array now owns its memory, so keeping one no longer keeps the
|
|
33
|
+
whole message alive.
|
|
34
|
+
|
|
3
35
|
## 4.1.1
|
|
4
36
|
|
|
5
37
|
- Unread messages no longer hold file descriptors on Linux and macOS: the sender unmaps
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-sharedmemory
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Fast multiprocessing.Queue alternative that moves large Python objects and NumPy arrays through shared memory
|
|
5
5
|
Author: Tobias Würth
|
|
6
6
|
License-Expression: MIT
|
|
@@ -68,10 +68,11 @@ images, and simulation states arrive up to 2× faster, and transfers too large f
|
|
|
68
68
|
|
|
69
69
|
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
70
70
|
`queue.Full`, and `queue.Empty`.
|
|
71
|
-
- **Any picklable object:** NumPy arrays
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
- **Any picklable object:** NumPy arrays, other protocol-5 buffers, and large `bytes` and
|
|
72
|
+
`bytearray` objects are copied straight into shared memory, without an intermediate
|
|
73
|
+
pickle copy.
|
|
74
|
+
- **Bounded memory:** a capacity limit applies backpressure, and received blocks are
|
|
75
|
+
reused for later messages, so shared memory stays within `capacity × message size`.
|
|
75
76
|
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
76
77
|
- **Portable:** Linux, macOS, and Windows; `spawn`, `fork`, and `forkserver`; Python 3.11–3.15.
|
|
77
78
|
- **Lightweight:** no dependencies, fully typed (PEP 561), and 100% test coverage.
|
|
@@ -131,8 +132,8 @@ arrays, image or video frames, RL rollouts, large `bytes`, or big nested objects
|
|
|
131
132
|
helps where large `multiprocessing.Queue` transfers cause heavy pipe I/O or uneven
|
|
132
133
|
throughput, which is especially noticeable on Windows.
|
|
133
134
|
|
|
134
|
-
Prefer `multiprocessing.Queue` for small messages. Below about 100 kB,
|
|
135
|
-
|
|
135
|
+
Prefer `multiprocessing.Queue` for small messages. Below about 100 kB, the fixed cost of
|
|
136
|
+
mapping a shared-memory block for every message outweighs the pipe.
|
|
136
137
|
|
|
137
138
|
To let several processes read and write **the same** array in place, use
|
|
138
139
|
[`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
|
|
@@ -155,20 +156,25 @@ Core i9-14900K, 191.8 GiB RAM, Windows 11, Python 3.12.10, NumPy 2.5.3, measured
|
|
|
155
156
|
2026-10-08. Contiguous float32 arrays, `spawn`, capacity 2, median of 5 runs excluding
|
|
156
157
|
startup and warmup; up to 1,000 messages or 512 MiB per run, at least one message.
|
|
157
158
|
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
|
+
These numbers show transfer speed; I/O load and stability depend on the workload. They
|
|
160
|
+
predate version 4.2, which reuses received blocks and so no longer pays to fault in fresh
|
|
161
|
+
pages on every `put`.
|
|
159
162
|
|
|
160
163
|
## How it works
|
|
161
164
|
|
|
162
|
-
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data and
|
|
166
|
+
`bytes` or `bytearray` objects of 64 KiB or more, never enter the pickle; everything is copied once,
|
|
167
|
+
from the original objects into one shared-memory block: a block from an earlier,
|
|
168
|
+
received message when one fits, otherwise a new one.
|
|
169
|
+
2. Only the block's name, buffer sizes, and a sequence number travel through a
|
|
170
|
+
`multiprocessing.Queue`.
|
|
165
171
|
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
166
172
|
sends an acknowledgement back.
|
|
167
|
-
4. A background thread in the producer
|
|
168
|
-
arrives. `capacity` limits how many
|
|
173
|
+
4. A background thread in the producer makes the block available for reuse when the
|
|
174
|
+
acknowledgement arrives. `capacity` limits how many messages can be outstanding.
|
|
169
175
|
|
|
170
176
|
Received objects never point into shared memory, so they stay valid after the block is
|
|
171
|
-
freed and can be modified freely.
|
|
177
|
+
reused or freed and can be modified freely.
|
|
172
178
|
|
|
173
179
|
## Usage guide
|
|
174
180
|
|
|
@@ -188,9 +194,15 @@ consumer = ctx.Process(target=consume, args=(receiver,))
|
|
|
188
194
|
|
|
189
195
|
### Capacity and backpressure
|
|
190
196
|
|
|
191
|
-
`capacity` counts messages that were sent but not yet received
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
`capacity` counts messages that were sent but not yet received. When it is reached,
|
|
198
|
+
`put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is at most
|
|
199
|
+
about `capacity × message size`. `capacity=0` or `None` means unbounded.
|
|
200
|
+
|
|
201
|
+
Received blocks are reused rather than freed, because writing into memory that is
|
|
202
|
+
already mapped is much faster than faulting in fresh pages. A block is reused for a
|
|
203
|
+
message that fills at least half of it, or for any message that fits a block of up to
|
|
204
|
+
1 MiB. The sender keeps at most 8 such blocks and 256 MiB, never more blocks than the
|
|
205
|
+
capacity allows, and frees them on `close()`. Blocks over 256 MiB are freed once received.
|
|
194
206
|
|
|
195
207
|
### Timeouts and non-blocking calls
|
|
196
208
|
|
|
@@ -241,7 +253,9 @@ such as `None` is a simple way to tell consumers that the stream has ended.
|
|
|
241
253
|
Arrays keep their dtype, shape, and writability. Read-only arrays stay read-only, and
|
|
242
254
|
writable ones arrive as writable local copies. C- and Fortran-contiguous arrays keep
|
|
243
255
|
their layout and are copied straight into shared memory without an intermediate pickle
|
|
244
|
-
copy; other views, such as strided slices, arrive as contiguous copies.
|
|
256
|
+
copy; other views, such as strided slices, arrive as contiguous copies. Each received
|
|
257
|
+
array has its own aligned memory, so keeping one array does not keep the rest of the
|
|
258
|
+
message alive.
|
|
245
259
|
|
|
246
260
|
### Errors
|
|
247
261
|
|
|
@@ -250,8 +264,9 @@ copy; other views, such as strided slices, arrive as contiguous copies.
|
|
|
250
264
|
| `queue.Full` | `put` found no capacity within `timeout` or with `block=False`. |
|
|
251
265
|
| `queue.Empty` | `get` found no message within `timeout` or with `block=False`. |
|
|
252
266
|
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
267
|
+
| `FileNotFoundError` | `get` received a message that the producer freed by closing before it was read. |
|
|
253
268
|
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
254
|
-
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still
|
|
269
|
+
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still released. |
|
|
255
270
|
| `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
|
|
256
271
|
|
|
257
272
|
## API reference
|
|
@@ -266,10 +281,10 @@ for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
|
266
281
|
|
|
267
282
|
| Member | Description |
|
|
268
283
|
| --- | --- |
|
|
269
|
-
| `put(obj, block=True, timeout=None)` | Copy `obj` into a
|
|
284
|
+
| `put(obj, block=True, timeout=None)` | Copy `obj` into a shared-memory block and send it. |
|
|
270
285
|
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
271
|
-
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and
|
|
272
|
-
| `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
|
|
286
|
+
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and its block released; `False` on timeout. |
|
|
287
|
+
| `close()` | Free unread messages and reusable blocks silently and release resources. Runs automatically at normal process exit. |
|
|
273
288
|
| `closed` | Whether the sender is closed. |
|
|
274
289
|
| `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
|
|
275
290
|
|
|
@@ -308,8 +323,9 @@ the last handle closes, so nothing can leak after a crash.
|
|
|
308
323
|
producer closed or exited before this message was read. Call `wait_for_all_ack()` before
|
|
309
324
|
closing the sender; the sender also warns when this happens.
|
|
310
325
|
|
|
311
|
-
**`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1
|
|
312
|
-
|
|
326
|
+
**`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1.
|
|
327
|
+
On Linux and macOS the sender keeps at most 8 blocks mapped for reuse, two file
|
|
328
|
+
descriptors each; other unread messages hold none.
|
|
313
329
|
|
|
314
330
|
**`RuntimeError: A shared-memory pair supports one producing process`:** a second
|
|
315
331
|
process called `put`. Create a separate pair for each producer process.
|
|
@@ -18,10 +18,11 @@ images, and simulation states arrive up to 2× faster, and transfers too large f
|
|
|
18
18
|
|
|
19
19
|
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
20
20
|
`queue.Full`, and `queue.Empty`.
|
|
21
|
-
- **Any picklable object:** NumPy arrays
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
- **Any picklable object:** NumPy arrays, other protocol-5 buffers, and large `bytes` and
|
|
22
|
+
`bytearray` objects are copied straight into shared memory, without an intermediate
|
|
23
|
+
pickle copy.
|
|
24
|
+
- **Bounded memory:** a capacity limit applies backpressure, and received blocks are
|
|
25
|
+
reused for later messages, so shared memory stays within `capacity × message size`.
|
|
25
26
|
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
26
27
|
- **Portable:** Linux, macOS, and Windows; `spawn`, `fork`, and `forkserver`; Python 3.11–3.15.
|
|
27
28
|
- **Lightweight:** no dependencies, fully typed (PEP 561), and 100% test coverage.
|
|
@@ -81,8 +82,8 @@ arrays, image or video frames, RL rollouts, large `bytes`, or big nested objects
|
|
|
81
82
|
helps where large `multiprocessing.Queue` transfers cause heavy pipe I/O or uneven
|
|
82
83
|
throughput, which is especially noticeable on Windows.
|
|
83
84
|
|
|
84
|
-
Prefer `multiprocessing.Queue` for small messages. Below about 100 kB,
|
|
85
|
-
|
|
85
|
+
Prefer `multiprocessing.Queue` for small messages. Below about 100 kB, the fixed cost of
|
|
86
|
+
mapping a shared-memory block for every message outweighs the pipe.
|
|
86
87
|
|
|
87
88
|
To let several processes read and write **the same** array in place, use
|
|
88
89
|
[`multiprocessing.shared_memory`](https://docs.python.org/3/library/multiprocessing.shared_memory.html)
|
|
@@ -105,20 +106,25 @@ Core i9-14900K, 191.8 GiB RAM, Windows 11, Python 3.12.10, NumPy 2.5.3, measured
|
|
|
105
106
|
2026-10-08. Contiguous float32 arrays, `spawn`, capacity 2, median of 5 runs excluding
|
|
106
107
|
startup and warmup; up to 1,000 messages or 512 MiB per run, at least one message.
|
|
107
108
|
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
|
+
These numbers show transfer speed; I/O load and stability depend on the workload. They
|
|
110
|
+
predate version 4.2, which reuses received blocks and so no longer pays to fault in fresh
|
|
111
|
+
pages on every `put`.
|
|
109
112
|
|
|
110
113
|
## How it works
|
|
111
114
|
|
|
112
|
-
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data and
|
|
116
|
+
`bytes` or `bytearray` objects of 64 KiB or more, never enter the pickle; everything is copied once,
|
|
117
|
+
from the original objects into one shared-memory block: a block from an earlier,
|
|
118
|
+
received message when one fits, otherwise a new one.
|
|
119
|
+
2. Only the block's name, buffer sizes, and a sequence number travel through a
|
|
120
|
+
`multiprocessing.Queue`.
|
|
115
121
|
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
116
122
|
sends an acknowledgement back.
|
|
117
|
-
4. A background thread in the producer
|
|
118
|
-
arrives. `capacity` limits how many
|
|
123
|
+
4. A background thread in the producer makes the block available for reuse when the
|
|
124
|
+
acknowledgement arrives. `capacity` limits how many messages can be outstanding.
|
|
119
125
|
|
|
120
126
|
Received objects never point into shared memory, so they stay valid after the block is
|
|
121
|
-
freed and can be modified freely.
|
|
127
|
+
reused or freed and can be modified freely.
|
|
122
128
|
|
|
123
129
|
## Usage guide
|
|
124
130
|
|
|
@@ -138,9 +144,15 @@ consumer = ctx.Process(target=consume, args=(receiver,))
|
|
|
138
144
|
|
|
139
145
|
### Capacity and backpressure
|
|
140
146
|
|
|
141
|
-
`capacity` counts messages that were sent but not yet received
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
`capacity` counts messages that were sent but not yet received. When it is reached,
|
|
148
|
+
`put()` blocks, so a fast producer cannot exhaust memory. Shared-memory use is at most
|
|
149
|
+
about `capacity × message size`. `capacity=0` or `None` means unbounded.
|
|
150
|
+
|
|
151
|
+
Received blocks are reused rather than freed, because writing into memory that is
|
|
152
|
+
already mapped is much faster than faulting in fresh pages. A block is reused for a
|
|
153
|
+
message that fills at least half of it, or for any message that fits a block of up to
|
|
154
|
+
1 MiB. The sender keeps at most 8 such blocks and 256 MiB, never more blocks than the
|
|
155
|
+
capacity allows, and frees them on `close()`. Blocks over 256 MiB are freed once received.
|
|
144
156
|
|
|
145
157
|
### Timeouts and non-blocking calls
|
|
146
158
|
|
|
@@ -191,7 +203,9 @@ such as `None` is a simple way to tell consumers that the stream has ended.
|
|
|
191
203
|
Arrays keep their dtype, shape, and writability. Read-only arrays stay read-only, and
|
|
192
204
|
writable ones arrive as writable local copies. C- and Fortran-contiguous arrays keep
|
|
193
205
|
their layout and are copied straight into shared memory without an intermediate pickle
|
|
194
|
-
copy; other views, such as strided slices, arrive as contiguous copies.
|
|
206
|
+
copy; other views, such as strided slices, arrive as contiguous copies. Each received
|
|
207
|
+
array has its own aligned memory, so keeping one array does not keep the rest of the
|
|
208
|
+
message alive.
|
|
195
209
|
|
|
196
210
|
### Errors
|
|
197
211
|
|
|
@@ -200,8 +214,9 @@ copy; other views, such as strided slices, arrive as contiguous copies.
|
|
|
200
214
|
| `queue.Full` | `put` found no capacity within `timeout` or with `block=False`. |
|
|
201
215
|
| `queue.Empty` | `get` found no message within `timeout` or with `block=False`. |
|
|
202
216
|
| `BrokenPipeError` | The endpoint is closed, or the producer's acknowledgement channel failed. |
|
|
217
|
+
| `FileNotFoundError` | `get` received a message that the producer freed by closing before it was read. |
|
|
203
218
|
| `RuntimeError` | A second process tries to produce, or a used sender is passed to another process. |
|
|
204
|
-
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still
|
|
219
|
+
| Pickling errors | The object cannot be pickled or unpickled. Both endpoints stay usable, and failed messages are still released. |
|
|
205
220
|
| `RuntimeWarning` (warning) | A `with sender:` block or the producing process ended normally and discarded unacknowledged messages. |
|
|
206
221
|
|
|
207
222
|
## API reference
|
|
@@ -216,10 +231,10 @@ for the underlying queues; it defaults to `multiprocessing.get_context()`.
|
|
|
216
231
|
|
|
217
232
|
| Member | Description |
|
|
218
233
|
| --- | --- |
|
|
219
|
-
| `put(obj, block=True, timeout=None)` | Copy `obj` into a
|
|
234
|
+
| `put(obj, block=True, timeout=None)` | Copy `obj` into a shared-memory block and send it. |
|
|
220
235
|
| `put_nowait(obj)` | `put(obj, block=False)`. |
|
|
221
|
-
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and
|
|
222
|
-
| `close()` | Free unread messages silently and release resources. Runs automatically at normal process exit. |
|
|
236
|
+
| `wait_for_all_ack(timeout=None) -> bool` | Wait until every message is received and its block released; `False` on timeout. |
|
|
237
|
+
| `close()` | Free unread messages and reusable blocks silently and release resources. Runs automatically at normal process exit. |
|
|
223
238
|
| `closed` | Whether the sender is closed. |
|
|
224
239
|
| `with sender:` | Closes the sender on exit; warns if a clean exit discards unacknowledged messages. |
|
|
225
240
|
|
|
@@ -258,8 +273,9 @@ the last handle closes, so nothing can leak after a crash.
|
|
|
258
273
|
producer closed or exited before this message was read. Call `wait_for_all_ack()` before
|
|
259
274
|
closing the sender; the sender also warns when this happens.
|
|
260
275
|
|
|
261
|
-
**`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1
|
|
262
|
-
|
|
276
|
+
**`OSError: [Errno 24] Too many open files` with many unread messages:** fixed in 4.1.1.
|
|
277
|
+
On Linux and macOS the sender keeps at most 8 blocks mapped for reuse, two file
|
|
278
|
+
descriptors each; other unread messages hold none.
|
|
263
279
|
|
|
264
280
|
**`RuntimeError: A shared-memory pair supports one producing process`:** a second
|
|
265
281
|
process called `put`. Create a separate pair for each producer process.
|
|
@@ -25,5 +25,5 @@ from .factory import create_shared_memory_pair
|
|
|
25
25
|
from .receiver import SharedMemoryReceiver
|
|
26
26
|
from .sender import SharedMemorySender
|
|
27
27
|
|
|
28
|
-
__version__ = "4.
|
|
28
|
+
__version__ = "4.2.1"
|
|
29
29
|
__all__ = ["SharedMemoryReceiver", "SharedMemorySender", "__version__", "create_shared_memory_pair"]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Serialize protocol-5 pickle data through a single shared-memory block."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import pickle
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from multiprocessing.shared_memory import SharedMemory
|
|
9
|
+
from typing import Any, NamedTuple, cast
|
|
10
|
+
|
|
11
|
+
# Receivers never unlink. Tracking their attachments would leak tracker entries and
|
|
12
|
+
# warnings when a receiver does not share the sender's resource tracker.
|
|
13
|
+
_ATTACH_OPTIONS: dict[str, Any] = {"track": False} if sys.version_info >= (3, 13) else {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SharedMemoryMessage(NamedTuple):
|
|
17
|
+
name: str
|
|
18
|
+
buffer_sizes: tuple[int, ...]
|
|
19
|
+
# Unique per sender. Blocks are reused, so acknowledgements name the message, not the block.
|
|
20
|
+
sequence: int = 0
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def size(self) -> int:
|
|
24
|
+
return sum(self.buffer_sizes)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_shared_memory(size: int) -> SharedMemory:
|
|
28
|
+
return SharedMemory(create=True, size=size)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def release_shared_memory(allocation: SharedMemory) -> None:
|
|
32
|
+
"""Release both the mapping and its name, including on error paths."""
|
|
33
|
+
try:
|
|
34
|
+
allocation.close()
|
|
35
|
+
finally:
|
|
36
|
+
with suppress(FileNotFoundError):
|
|
37
|
+
allocation.unlink()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _PickleChunks(list[memoryview]):
|
|
41
|
+
"""Pickle output kept as views of what pickle wrote rather than one joined copy."""
|
|
42
|
+
|
|
43
|
+
def write(self, data: Any) -> None:
|
|
44
|
+
# Pickle passes bytes and bytearrays of 64 KiB or more as the objects themselves.
|
|
45
|
+
# A view also stops a bytearray from being resized before it is copied, which would
|
|
46
|
+
# leave its length header in the stream wrong.
|
|
47
|
+
self.append(memoryview(data))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def serialize(
|
|
51
|
+
data: Any,
|
|
52
|
+
sequence: int = 0,
|
|
53
|
+
allocate: Callable[[int], SharedMemory] = create_shared_memory,
|
|
54
|
+
) -> tuple[SharedMemory, SharedMemoryMessage]:
|
|
55
|
+
"""Copy a pickle and its out-of-band buffers into an owned block of at least that size.
|
|
56
|
+
|
|
57
|
+
Large bytes, bytearrays, and contiguous arrays are copied once, straight from the
|
|
58
|
+
original objects into shared memory. `allocate` returns a new or reused block; this
|
|
59
|
+
function frees it if copying fails.
|
|
60
|
+
"""
|
|
61
|
+
chunks = _PickleChunks()
|
|
62
|
+
buffers: list[memoryview] = []
|
|
63
|
+
allocation = None
|
|
64
|
+
|
|
65
|
+
def append_buffer(buffer: pickle.PickleBuffer) -> None:
|
|
66
|
+
buffers.append(buffer.raw())
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
pickle.Pickler(chunks, protocol=5, buffer_callback=append_buffer).dump(data)
|
|
70
|
+
pickle_size = sum(chunk.nbytes for chunk in chunks)
|
|
71
|
+
buffer_sizes = (pickle_size, *(buffer.nbytes for buffer in buffers))
|
|
72
|
+
allocation = allocate(sum(buffer_sizes))
|
|
73
|
+
shared = cast(memoryview, allocation.buf)
|
|
74
|
+
offset = 0
|
|
75
|
+
for view in (*chunks, *buffers):
|
|
76
|
+
shared[offset : offset + view.nbytes] = view
|
|
77
|
+
offset += view.nbytes
|
|
78
|
+
return allocation, SharedMemoryMessage(allocation.name, buffer_sizes, sequence)
|
|
79
|
+
except BaseException:
|
|
80
|
+
if allocation is not None:
|
|
81
|
+
release_shared_memory(allocation)
|
|
82
|
+
raise
|
|
83
|
+
finally:
|
|
84
|
+
for view in (*chunks, *buffers):
|
|
85
|
+
view.release()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def deserialize(message: SharedMemoryMessage) -> Any:
|
|
89
|
+
"""Unpickle with owned local buffers and always close the shared mapping."""
|
|
90
|
+
return load(message, attach(message))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def attach(message: SharedMemoryMessage) -> SharedMemory:
|
|
94
|
+
"""Map the block that valid metadata names, or explain why it no longer exists."""
|
|
95
|
+
_validate_message(message)
|
|
96
|
+
try:
|
|
97
|
+
allocation = SharedMemory(name=message.name, **_ATTACH_OPTIONS)
|
|
98
|
+
except FileNotFoundError as error:
|
|
99
|
+
raise _freed_block_error(message) from error
|
|
100
|
+
# Attaching on Windows is not atomic: a name freed in between is recreated as a new,
|
|
101
|
+
# zero-filled block. Every pickle starts with a nonzero opcode.
|
|
102
|
+
if cast(memoryview, allocation.buf)[0] == 0:
|
|
103
|
+
allocation.close()
|
|
104
|
+
raise _freed_block_error(message)
|
|
105
|
+
return allocation
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _freed_block_error(message: SharedMemoryMessage) -> FileNotFoundError:
|
|
109
|
+
return FileNotFoundError(
|
|
110
|
+
errno.ENOENT,
|
|
111
|
+
"Shared-memory block was freed before it was received; the sender probably "
|
|
112
|
+
"closed without calling wait_for_all_ack()",
|
|
113
|
+
message.name,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load(message: SharedMemoryMessage, allocation: SharedMemory) -> Any:
|
|
118
|
+
"""Unpickle an attached block with owned local buffers, then close the mapping."""
|
|
119
|
+
try:
|
|
120
|
+
if message.size > allocation.size:
|
|
121
|
+
raise ValueError("Shared-memory block is smaller than its metadata")
|
|
122
|
+
shared = cast(memoryview, allocation.buf)
|
|
123
|
+
pickle_size = message.buffer_sizes[0]
|
|
124
|
+
buffers = _copy_buffers(shared, pickle_size, message.buffer_sizes[1:])
|
|
125
|
+
with shared[:pickle_size] as pickle_data:
|
|
126
|
+
return pickle.loads(pickle_data, buffers=buffers)
|
|
127
|
+
finally:
|
|
128
|
+
allocation.close()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _validate_message(message: SharedMemoryMessage) -> None:
|
|
132
|
+
if not isinstance(message, SharedMemoryMessage) or not isinstance(message.name, str):
|
|
133
|
+
raise ValueError("Invalid shared-memory metadata")
|
|
134
|
+
sizes = message.buffer_sizes
|
|
135
|
+
if (
|
|
136
|
+
not message.name
|
|
137
|
+
or type(message.sequence) is not int
|
|
138
|
+
or message.sequence < 0
|
|
139
|
+
or not isinstance(sizes, tuple)
|
|
140
|
+
or not sizes
|
|
141
|
+
or any(type(size) is not int or size < 0 for size in sizes)
|
|
142
|
+
or sizes[0] == 0
|
|
143
|
+
):
|
|
144
|
+
raise ValueError("Invalid shared-memory metadata")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _copy_buffers(shared: memoryview, offset: int, sizes: tuple[int, ...]) -> list[memoryview]:
|
|
148
|
+
# A separate writable copy per buffer starts aligned for any dtype and is freed on its own,
|
|
149
|
+
# so a small array cannot keep a large neighbour alive.
|
|
150
|
+
buffers = []
|
|
151
|
+
for size in sizes:
|
|
152
|
+
with shared[offset : offset + size] as shared_buffer:
|
|
153
|
+
buffers.append(memoryview(bytearray(shared_buffer)))
|
|
154
|
+
offset += size
|
|
155
|
+
return buffers
|
|
@@ -6,18 +6,18 @@ from multiprocessing.queues import Queue
|
|
|
6
6
|
from types import TracebackType
|
|
7
7
|
from typing import Any, Self
|
|
8
8
|
|
|
9
|
-
from .convert import SharedMemoryMessage,
|
|
9
|
+
from .convert import SharedMemoryMessage, attach, load
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SharedMemoryReceiver:
|
|
13
13
|
"""Consuming endpoint of a pair; create it with `create_shared_memory_pair`.
|
|
14
14
|
|
|
15
15
|
Several threads and processes may share one receiver; each message is delivered to
|
|
16
|
-
exactly one of them. Every
|
|
17
|
-
the sender can free its memory.
|
|
16
|
+
exactly one of them. Every received block is acknowledged, even when decoding fails,
|
|
17
|
+
so the sender can free its memory.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
def __init__(self, data_queue: Queue[SharedMemoryMessage], ack_queue: Queue[
|
|
20
|
+
def __init__(self, data_queue: Queue[SharedMemoryMessage], ack_queue: Queue[int]) -> None:
|
|
21
21
|
self._data_queue = data_queue
|
|
22
22
|
self._ack_queue = ack_queue
|
|
23
23
|
self._closed = False
|
|
@@ -35,24 +35,26 @@ class SharedMemoryReceiver:
|
|
|
35
35
|
timeout: Maximum seconds to wait; `None` waits forever.
|
|
36
36
|
|
|
37
37
|
Returns:
|
|
38
|
-
The object, which stays valid after the
|
|
38
|
+
The object, which stays valid after the block is reused or freed. Arrays keep
|
|
39
39
|
their original writability.
|
|
40
40
|
|
|
41
41
|
Raises:
|
|
42
42
|
queue.Empty: No message arrived in time.
|
|
43
43
|
BrokenPipeError: The receiver is closed.
|
|
44
|
+
FileNotFoundError: The sender freed the message before it was received.
|
|
44
45
|
|
|
45
46
|
Unpickling errors propagate unchanged; the message is still acknowledged.
|
|
46
47
|
"""
|
|
47
48
|
if self._closed:
|
|
48
49
|
raise BrokenPipeError("Receiver is closed")
|
|
49
50
|
message = self._data_queue.get(block, timeout)
|
|
51
|
+
# A block that is gone was already freed by its sender, which expects no acknowledgement.
|
|
52
|
+
allocation = attach(message)
|
|
50
53
|
try:
|
|
51
|
-
return
|
|
54
|
+
return load(message, allocation)
|
|
52
55
|
finally:
|
|
53
56
|
# Failed decoding must still release the sender's memory and capacity.
|
|
54
|
-
|
|
55
|
-
self._ack_queue.put_nowait(message.name)
|
|
57
|
+
self._ack_queue.put_nowait(message.sequence)
|
|
56
58
|
|
|
57
59
|
def get_nowait(self) -> Any:
|
|
58
60
|
"""Equivalent to `get(block=False)`."""
|