Skip to content

Commit 81ee026

Browse files
pan324ambvgvanrossumpitrougpshead
authored
gh-82300: Add track parameter to multiprocessing.shared_memory (#110778)
Add a track parameter to shared memory to allow resource tracking via the side-launched resource tracker process to be disabled on platforms that use it (POSIX). This allows people who do not want automated cleanup at process exit because they are using the shared memory with processes not participating in Python's resource tracking to use the shared_memory API. Co-authored-by: Łukasz Langa <[email protected]> Co-authored-by: Guido van Rossum <[email protected]> Co-authored-by: Antoine Pitrou <[email protected]> Co-authored-by: Gregory P. Smith <[email protected]>
1 parent 9f92b31 commit 81ee026

File tree

4 files changed

+106
-23
lines changed

4 files changed

+106
-23
lines changed

Doc/library/multiprocessing.shared_memory.rst

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ or other communications requiring the serialization/deserialization and
3636
copying of data.
3737

3838

39-
.. class:: SharedMemory(name=None, create=False, size=0)
39+
.. class:: SharedMemory(name=None, create=False, size=0, *, track=True)
4040

4141
Creates a new shared memory block or attaches to an existing shared
4242
memory block. Each shared memory block is assigned a unique name.
@@ -64,26 +64,45 @@ copying of data.
6464
memory block may be larger or equal to the size requested. When attaching
6565
to an existing shared memory block, the ``size`` parameter is ignored.
6666

67+
*track*, when enabled, registers the shared memory block with a resource
68+
tracker process on platforms where the OS does not do this automatically.
69+
The resource tracker ensures proper cleanup of the shared memory even
70+
if all other processes with access to the memory exit without doing so.
71+
Python processes created from a common ancestor using :mod:`multiprocessing`
72+
facilities share a single resource tracker process, and the lifetime of
73+
shared memory segments is handled automatically among these processes.
74+
Python processes created in any other way will receive their own
75+
resource tracker when accessing shared memory with *track* enabled.
76+
This will cause the shared memory to be deleted by the resource tracker
77+
of the first process that terminates.
78+
To avoid this issue, users of :mod:`subprocess` or standalone Python
79+
processes should set *track* to ``False`` when there is already another
80+
process in place that does the bookkeeping.
81+
*track* is ignored on Windows, which has its own tracking and
82+
automatically deletes shared memory when all handles to it have been closed.
83+
84+
.. versionchanged:: 3.13 Added *track* parameter.
85+
6786
.. method:: close()
6887

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.
88+
Closes the file descriptor/handle to the shared memory from this
89+
instance. :meth:`close()` should be called once access to the shared
90+
memory block from this instance is no longer needed. Depending
91+
on operating system, the underlying memory may or may not be freed
92+
even if all handles to it have been closed. To ensure proper cleanup,
93+
use the :meth:`unlink()` method.
7494

7595
.. method:: unlink()
7696

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.
97+
Deletes the underlying shared memory block. This should be called only
98+
once per shared memory block regardless of the number of handles to it,
99+
even in other processes.
100+
:meth:`unlink()` and :meth:`close()` can be called in any order, but
101+
trying to access data inside a shared memory block after :meth:`unlink()`
102+
may result in memory access errors, depending on platform.
103+
104+
This method has no effect on Windows, where the only way to delete a
105+
shared memory block is to close all handles.
87106

88107
.. attribute:: buf
89108

Lib/multiprocessing/shared_memory.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ class SharedMemory:
7171
_flags = os.O_RDWR
7272
_mode = 0o600
7373
_prepend_leading_slash = True if _USE_POSIX else False
74+
_track = True
7475

75-
def __init__(self, name=None, create=False, size=0):
76+
def __init__(self, name=None, create=False, size=0, *, track=True):
7677
if not size >= 0:
7778
raise ValueError("'size' must be a positive integer")
7879
if create:
@@ -82,6 +83,7 @@ def __init__(self, name=None, create=False, size=0):
8283
if name is None and not self._flags & os.O_EXCL:
8384
raise ValueError("'name' can only be None if create=True")
8485

86+
self._track = track
8587
if _USE_POSIX:
8688

8789
# POSIX Shared Memory
@@ -116,8 +118,8 @@ def __init__(self, name=None, create=False, size=0):
116118
except OSError:
117119
self.unlink()
118120
raise
119-
120-
resource_tracker.register(self._name, "shared_memory")
121+
if self._track:
122+
resource_tracker.register(self._name, "shared_memory")
121123

122124
else:
123125

@@ -236,12 +238,20 @@ def close(self):
236238
def unlink(self):
237239
"""Requests that the underlying shared memory block be destroyed.
238240
239-
In order to ensure proper cleanup of resources, unlink should be
240-
called once (and only once) across all processes which have access
241-
to the shared memory block."""
241+
Unlink should be called once (and only once) across all handles
242+
which have access to the shared memory block, even if these
243+
handles belong to different processes. Closing and unlinking may
244+
happen in any order, but trying to access data inside a shared
245+
memory block after unlinking may result in memory errors,
246+
depending on platform.
247+
248+
This method has no effect on Windows, where the only way to
249+
delete a shared memory block is to close all handles."""
250+
242251
if _USE_POSIX and self._name:
243252
_posixshmem.shm_unlink(self._name)
244-
resource_tracker.unregister(self._name, "shared_memory")
253+
if self._track:
254+
resource_tracker.unregister(self._name, "shared_memory")
245255

246256

247257
_encoding = "utf8"

Lib/test/_test_multiprocessing.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4455,6 +4455,59 @@ def test_shared_memory_cleaned_after_process_termination(self):
44554455
"resource_tracker: There appear to be 1 leaked "
44564456
"shared_memory objects to clean up at shutdown", err)
44574457

4458+
@unittest.skipIf(os.name != "posix", "resource_tracker is posix only")
4459+
def test_shared_memory_untracking(self):
4460+
# gh-82300: When a separate Python process accesses shared memory
4461+
# with track=False, it must not cause the memory to be deleted
4462+
# when terminating.
4463+
cmd = '''if 1:
4464+
import sys
4465+
from multiprocessing.shared_memory import SharedMemory
4466+
mem = SharedMemory(create=False, name=sys.argv[1], track=False)
4467+
mem.close()
4468+
'''
4469+
mem = shared_memory.SharedMemory(create=True, size=10)
4470+
# The resource tracker shares pipes with the subprocess, and so
4471+
# err existing means that the tracker process has terminated now.
4472+
try:
4473+
rc, out, err = script_helper.assert_python_ok("-c", cmd, mem.name)
4474+
self.assertNotIn(b"resource_tracker", err)
4475+
self.assertEqual(rc, 0)
4476+
mem2 = shared_memory.SharedMemory(create=False, name=mem.name)
4477+
mem2.close()
4478+
finally:
4479+
try:
4480+
mem.unlink()
4481+
except OSError:
4482+
pass
4483+
mem.close()
4484+
4485+
@unittest.skipIf(os.name != "posix", "resource_tracker is posix only")
4486+
def test_shared_memory_tracking(self):
4487+
# gh-82300: When a separate Python process accesses shared memory
4488+
# with track=True, it must cause the memory to be deleted when
4489+
# terminating.
4490+
cmd = '''if 1:
4491+
import sys
4492+
from multiprocessing.shared_memory import SharedMemory
4493+
mem = SharedMemory(create=False, name=sys.argv[1], track=True)
4494+
mem.close()
4495+
'''
4496+
mem = shared_memory.SharedMemory(create=True, size=10)
4497+
try:
4498+
rc, out, err = script_helper.assert_python_ok("-c", cmd, mem.name)
4499+
self.assertEqual(rc, 0)
4500+
self.assertIn(
4501+
b"resource_tracker: There appear to be 1 leaked "
4502+
b"shared_memory objects to clean up at shutdown", err)
4503+
finally:
4504+
try:
4505+
mem.unlink()
4506+
except OSError:
4507+
pass
4508+
resource_tracker.unregister(mem._name, "shared_memory")
4509+
mem.close()
4510+
44584511
#
44594512
# Test to verify that `Finalize` works.
44604513
#
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ``track`` parameter to :class:`multiprocessing.shared_memory.SharedMemory` that allows using shared memory blocks without having to register with the POSIX resource tracker that automatically releases them upon process exit.

0 commit comments

Comments
 (0)