Skip to content

Commit e895de3

Browse files
authored
bpo-35813: Tests and docs for shared_memory (#11816)
* Added tests for shared_memory submodule. * Added tests for ShareableList. * Fix bug in allocationn size during creation of empty ShareableList illuminated by existing test run on Linux. * Initial set of docs for shared_memory module. * Added docs for ShareableList, added doctree entry for shared_memory submodule, name refactoring for greater clarity. * Added examples to SharedMemoryManager docs, for ease of documentation switched away from exclusively registered functions to some explicit methods on SharedMemoryManager. * Wording tweaks to docs. * Fix test failures on Windows. * Added tests around SharedMemoryManager. * Documentation tweaks. * Fix inappropriate test on Windows. * Further documentation tweaks. * Fix bare exception. * Removed __copyright__. * Fixed typo in doc, removed comment. * Updated SharedMemoryManager preliminary tests to reflect change of not supporting all registered functions on SyncManager. * Added Sphinx doctest run controls. * CloseHandle should be in a finally block in case MapViewOfFile fails. * Missed opportunity to use with statement. * Switch to self.addCleanup to spare long try/finally blocks and save one indentation, change to use decorator to skip test instead. * Simplify the posixshmem extension module. Provide shm_open() and shm_unlink() functions. Move other functionality into the shared_memory.py module. * Added to doc around size parameter of SharedMemory. * Changed PosixSharedMemory.size to use os.fstat. * Change SharedMemory.buf to a read-only property as well as NamedSharedMemory.size. * Marked as provisional per PEP411 in docstring. * Changed SharedMemoryTracker to be private. * Removed registered Proxy Objects from SharedMemoryManager. * Removed shareable_wrap(). * Removed shareable_wrap() and dangling references to it. * For consistency added __reduce__ to key classes. * Fix for potential race condition on Windows for O_CREX. * Remove unused imports. * Update access to kernel32 on Windows per feedback from eryksun. * Moved kernel32 calls to _winapi. * Removed ShareableList.copy as redundant. * Changes to _winapi use from eryksun feedback. * Adopt simpler SharedMemory API, collapsing PosixSharedMemory and WindowsNamedSharedMemory into one. * Fix missing docstring on class, add test for ignoring size when attaching. * Moved SharedMemoryManager to managers module, tweak to fragile test. * Tweak to exception in OpenFileMapping suggested by eryksun. * Mark a few dangling bits as private as suggested by Giampaolo.
1 parent d610116 commit e895de3

File tree

9 files changed

+1504
-1022
lines changed

9 files changed

+1504
-1022
lines changed

Doc/library/concurrency.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ multitasking). Here's an overview:
1515

1616
threading.rst
1717
multiprocessing.rst
18+
multiprocessing.shared_memory.rst
1819
concurrent.rst
1920
concurrent.futures.rst
2021
subprocess.rst
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
:mod:`multiprocessing.shared_memory` --- Provides shared memory for direct access across processes
2+
===================================================================================================
3+
4+
.. module:: multiprocessing.shared_memory
5+
:synopsis: Provides shared memory for direct access across processes.
6+
7+
**Source code:** :source:`Lib/multiprocessing/shared_memory.py`
8+
9+
.. versionadded:: 3.8
10+
11+
.. index::
12+
single: Shared Memory
13+
single: POSIX Shared Memory
14+
single: Named Shared Memory
15+
16+
--------------
17+
18+
This module provides a class, :class:`SharedMemory`, for the allocation
19+
and management of shared memory to be accessed by one or more processes
20+
on a multicore or symmetric multiprocessor (SMP) machine. To assist with
21+
the life-cycle management of shared memory especially across distinct
22+
processes, a :class:`~multiprocessing.managers.BaseManager` subclass,
23+
:class:`SharedMemoryManager`, is also provided in the
24+
``multiprocessing.managers`` module.
25+
26+
In this module, shared memory refers to "System V style" shared memory blocks
27+
(though is not necessarily implemented explicitly as such) and does not refer
28+
to "distributed shared memory". This style of shared memory permits distinct
29+
processes to potentially read and write to a common (or shared) region of
30+
volatile memory. Processes are conventionally limited to only have access to
31+
their own process memory space but shared memory permits the sharing
32+
of data between processes, avoiding the need to instead send messages between
33+
processes containing that data. Sharing data directly via memory can provide
34+
significant performance benefits compared to sharing data via disk or socket
35+
or other communications requiring the serialization/deserialization and
36+
copying of data.
37+
38+
39+
.. class:: SharedMemory(name=None, create=False, size=0)
40+
41+
Creates a new shared memory block or attaches to an existing shared
42+
memory block. Each shared memory block is assigned a unique name.
43+
In this way, one process can create a shared memory block with a
44+
particular name and a different process can attach to that same shared
45+
memory block using that same name.
46+
47+
As a resource for sharing data across processes, shared memory blocks
48+
may outlive the original process that created them. When one process
49+
no longer needs access to a shared memory block that might still be
50+
needed by other processes, the :meth:`close()` method should be called.
51+
When a shared memory block is no longer needed by any process, the
52+
:meth:`unlink()` method should be called to ensure proper cleanup.
53+
54+
*name* is the unique name for the requested shared memory, specified as
55+
a string. When creating a new shared memory block, if ``None`` (the
56+
default) is supplied for the name, a novel name will be generated.
57+
58+
*create* controls whether a new shared memory block is created (``True``)
59+
or an existing shared memory block is attached (``False``).
60+
61+
*size* specifies the requested number of bytes when creating a new shared
62+
memory block. Because some platforms choose to allocate chunks of memory
63+
based upon that platform's memory page size, the exact size of the shared
64+
memory block may be larger or equal to the size requested. When attaching
65+
to an existing shared memory block, the ``size`` parameter is ignored.
66+
67+
.. method:: close()
68+
69+
Closes access to the shared memory from this instance. In order to
70+
ensure proper cleanup of resources, all instances should call
71+
``close()`` once the instance is no longer needed. Note that calling
72+
``close()`` does not cause the shared memory block itself to be
73+
destroyed.
74+
75+
.. method:: unlink()
76+
77+
Requests that the underlying shared memory block be destroyed. In
78+
order to ensure proper cleanup of resources, ``unlink()`` should be
79+
called once (and only once) across all processes which have need
80+
for the shared memory block. After requesting its destruction, a
81+
shared memory block may or may not be immediately destroyed and
82+
this behavior may differ across platforms. Attempts to access data
83+
inside the shared memory block after ``unlink()`` has been called may
84+
result in memory access errors. Note: the last process relinquishing
85+
its hold on a shared memory block may call ``unlink()`` and
86+
:meth:`close()` in either order.
87+
88+
.. attribute:: buf
89+
90+
A memoryview of contents of the shared memory block.
91+
92+
.. attribute:: name
93+
94+
Read-only access to the unique name of the shared memory block.
95+
96+
.. attribute:: size
97+
98+
Read-only access to size in bytes of the shared memory block.
99+
100+
101+
The following example demonstrates low-level use of :class:`SharedMemory`
102+
instances::
103+
104+
>>> from multiprocessing import shared_memory
105+
>>> shm_a = shared_memory.SharedMemory(create=True, size=10)
106+
>>> type(shm_a.buf)
107+
<class 'memoryview'>
108+
>>> buffer = shm_a.buf
109+
>>> len(buffer)
110+
10
111+
>>> buffer[:4] = bytearray([22, 33, 44, 55]) # Modify multiple at once
112+
>>> buffer[4] = 100 # Modify single byte at a time
113+
>>> # Attach to an existing shared memory block
114+
>>> shm_b = shared_memory.SharedMemory(shm_a.name)
115+
>>> import array
116+
>>> array.array('b', shm_b.buf[:5]) # Copy the data into a new array.array
117+
array('b', [22, 33, 44, 55, 100])
118+
>>> shm_b.buf[:5] = b'howdy' # Modify via shm_b using bytes
119+
>>> bytes(shm_a.buf[:5]) # Access via shm_a
120+
b'howdy'
121+
>>> shm_b.close() # Close each SharedMemory instance
122+
>>> shm_a.close()
123+
>>> shm_a.unlink() # Call unlink only once to release the shared memory
124+
125+
126+
127+
The following example demonstrates a practical use of the :class:`SharedMemory`
128+
class with `NumPy arrays <https://www.numpy.org/>`_, accessing the
129+
same ``numpy.ndarray`` from two distinct Python shells:
130+
131+
.. doctest::
132+
:options: +SKIP
133+
134+
>>> # In the first Python interactive shell
135+
>>> import numpy as np
136+
>>> a = np.array([1, 1, 2, 3, 5, 8]) # Start with an existing NumPy array
137+
>>> from multiprocessing import shared_memory
138+
>>> shm = shared_memory.SharedMemory(create=True, size=a.nbytes)
139+
>>> # Now create a NumPy array backed by shared memory
140+
>>> b = np.ndarray(a.shape, dtype=a.dtype, buffer=shm.buf)
141+
>>> b[:] = a[:] # Copy the original data into shared memory
142+
>>> b
143+
array([1, 1, 2, 3, 5, 8])
144+
>>> type(b)
145+
<class 'numpy.ndarray'>
146+
>>> type(a)
147+
<class 'numpy.ndarray'>
148+
>>> shm.name # We did not specify a name so one was chosen for us
149+
'psm_21467_46075'
150+
151+
>>> # In either the same shell or a new Python shell on the same machine
152+
>>> import numpy as np
153+
>>> from multiprocessing import shared_memory
154+
>>> # Attach to the existing shared memory block
155+
>>> existing_shm = shared_memory.SharedMemory(name='psm_21467_46075')
156+
>>> # Note that a.shape is (6,) and a.dtype is np.int64 in this example
157+
>>> c = np.ndarray((6,), dtype=np.int64, buffer=existing_shm.buf)
158+
>>> c
159+
array([1, 1, 2, 3, 5, 8])
160+
>>> c[-1] = 888
161+
>>> c
162+
array([ 1, 1, 2, 3, 5, 888])
163+
164+
>>> # Back in the first Python interactive shell, b reflects this change
165+
>>> b
166+
array([ 1, 1, 2, 3, 5, 888])
167+
168+
>>> # Clean up from within the second Python shell
169+
>>> del c # Unnecessary; merely emphasizing the array is no longer used
170+
>>> existing_shm.close()
171+
172+
>>> # Clean up from within the first Python shell
173+
>>> del b # Unnecessary; merely emphasizing the array is no longer used
174+
>>> shm.close()
175+
>>> shm.unlink() # Free and release the shared memory block at the very end
176+
177+
178+
.. class:: SharedMemoryManager([address[, authkey]])
179+
180+
A subclass of :class:`~multiprocessing.managers.BaseManager` which can be
181+
used for the management of shared memory blocks across processes.
182+
183+
A call to :meth:`~multiprocessing.managers.BaseManager.start` on a
184+
:class:`SharedMemoryManager` instance causes a new process to be started.
185+
This new process's sole purpose is to manage the life cycle
186+
of all shared memory blocks created through it. To trigger the release
187+
of all shared memory blocks managed by that process, call
188+
:meth:`~multiprocessing.managers.BaseManager.shutdown()` on the instance.
189+
This triggers a :meth:`SharedMemory.unlink()` call on all of the
190+
:class:`SharedMemory` objects managed by that process and then
191+
stops the process itself. By creating ``SharedMemory`` instances
192+
through a ``SharedMemoryManager``, we avoid the need to manually track
193+
and trigger the freeing of shared memory resources.
194+
195+
This class provides methods for creating and returning :class:`SharedMemory`
196+
instances and for creating a list-like object (:class:`ShareableList`)
197+
backed by shared memory.
198+
199+
Refer to :class:`multiprocessing.managers.BaseManager` for a description
200+
of the inherited *address* and *authkey* optional input arguments and how
201+
they may be used to connect to an existing ``SharedMemoryManager`` service
202+
from other processes.
203+
204+
.. method:: SharedMemory(size)
205+
206+
Create and return a new :class:`SharedMemory` object with the
207+
specified ``size`` in bytes.
208+
209+
.. method:: ShareableList(sequence)
210+
211+
Create and return a new :class:`ShareableList` object, initialized
212+
by the values from the input ``sequence``.
213+
214+
215+
The following example demonstrates the basic mechanisms of a
216+
:class:`SharedMemoryManager`:
217+
218+
.. doctest::
219+
:options: +SKIP
220+
221+
>>> from multiprocessing import shared_memory
222+
>>> smm = shared_memory.SharedMemoryManager()
223+
>>> smm.start() # Start the process that manages the shared memory blocks
224+
>>> sl = smm.ShareableList(range(4))
225+
>>> sl
226+
ShareableList([0, 1, 2, 3], name='psm_6572_7512')
227+
>>> raw_shm = smm.SharedMemory(size=128)
228+
>>> another_sl = smm.ShareableList('alpha')
229+
>>> another_sl
230+
ShareableList(['a', 'l', 'p', 'h', 'a'], name='psm_6572_12221')
231+
>>> smm.shutdown() # Calls unlink() on sl, raw_shm, and another_sl
232+
233+
The following example depicts a potentially more convenient pattern for using
234+
:class:`SharedMemoryManager` objects via the :keyword:`with` statement to
235+
ensure that all shared memory blocks are released after they are no longer
236+
needed:
237+
238+
.. doctest::
239+
:options: +SKIP
240+
241+
>>> with shared_memory.SharedMemoryManager() as smm:
242+
... sl = smm.ShareableList(range(2000))
243+
... # Divide the work among two processes, storing partial results in sl
244+
... p1 = Process(target=do_work, args=(sl, 0, 1000))
245+
... p2 = Process(target=do_work, args=(sl, 1000, 2000))
246+
... p1.start()
247+
... p2.start() # A multiprocessing.Pool might be more efficient
248+
... p1.join()
249+
... p2.join() # Wait for all work to complete in both processes
250+
... total_result = sum(sl) # Consolidate the partial results now in sl
251+
252+
When using a :class:`SharedMemoryManager` in a :keyword:`with` statement, the
253+
shared memory blocks created using that manager are all released when the
254+
:keyword:`with` statement's code block finishes execution.
255+
256+
257+
.. class:: ShareableList(sequence=None, *, name=None)
258+
259+
Provides a mutable list-like object where all values stored within are
260+
stored in a shared memory block. This constrains storable values to
261+
only the ``int``, ``float``, ``bool``, ``str`` (less than 10M bytes each),
262+
``bytes`` (less than 10M bytes each), and ``None`` built-in data types.
263+
It also notably differs from the built-in ``list`` type in that these
264+
lists can not change their overall length (i.e. no append, insert, etc.)
265+
and do not support the dynamic creation of new :class:`ShareableList`
266+
instances via slicing.
267+
268+
*sequence* is used in populating a new ``ShareableList`` full of values.
269+
Set to ``None`` to instead attach to an already existing
270+
``ShareableList`` by its unique shared memory name.
271+
272+
*name* is the unique name for the requested shared memory, as described
273+
in the definition for :class:`SharedMemory`. When attaching to an
274+
existing ``ShareableList``, specify its shared memory block's unique
275+
name while leaving ``sequence`` set to ``None``.
276+
277+
.. method:: count(value)
278+
279+
Returns the number of occurrences of ``value``.
280+
281+
.. method:: index(value)
282+
283+
Returns first index position of ``value``. Raises :exc:`ValueError` if
284+
``value`` is not present.
285+
286+
.. attribute:: format
287+
288+
Read-only attribute containing the :mod:`struct` packing format used by
289+
all currently stored values.
290+
291+
.. attribute:: shm
292+
293+
The :class:`SharedMemory` instance where the values are stored.
294+
295+
296+
The following example demonstrates basic use of a :class:`ShareableList`
297+
instance:
298+
299+
>>> from multiprocessing import shared_memory
300+
>>> a = shared_memory.ShareableList(['howdy', b'HoWdY', -273.154, 100, None, True, 42])
301+
>>> [ type(entry) for entry in a ]
302+
[<class 'str'>, <class 'bytes'>, <class 'float'>, <class 'int'>, <class 'NoneType'>, <class 'bool'>, <class 'int'>]
303+
>>> a[2]
304+
-273.154
305+
>>> a[2] = -78.5
306+
>>> a[2]
307+
-78.5
308+
>>> a[2] = 'dry ice' # Changing data types is supported as well
309+
>>> a[2]
310+
'dry ice'
311+
>>> a[2] = 'larger than previously allocated storage space'
312+
Traceback (most recent call last):
313+
...
314+
ValueError: exceeds available storage for existing str
315+
>>> a[2]
316+
'dry ice'
317+
>>> len(a)
318+
7
319+
>>> a.index(42)
320+
6
321+
>>> a.count(b'howdy')
322+
0
323+
>>> a.count(b'HoWdY')
324+
1
325+
>>> a.shm.close()
326+
>>> a.shm.unlink()
327+
>>> del a # Use of a ShareableList after call to unlink() is unsupported
328+
329+
The following example depicts how one, two, or many processes may access the
330+
same :class:`ShareableList` by supplying the name of the shared memory block
331+
behind it:
332+
333+
>>> b = shared_memory.ShareableList(range(5)) # In a first process
334+
>>> c = shared_memory.ShareableList(name=b.shm.name) # In a second process
335+
>>> c
336+
ShareableList([0, 1, 2, 3, 4], name='...')
337+
>>> c[-1] = -999
338+
>>> b[-1]
339+
-999
340+
>>> b.shm.close()
341+
>>> c.shm.close()
342+
>>> c.shm.unlink()
343+

0 commit comments

Comments
 (0)