-
-
Notifications
You must be signed in to change notification settings - Fork 31.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
resource tracker destroys shared memory segments when other processes should still have valid access #82300
Comments
The resource tracker currently destroys (via _posixshmem.shm_unlink) shared memory segments on posix systems when any independently created Python process with a handle on a shared memory segment exits (gracefully or otherwise). This breaks the expected cross-platform behavior that a shared memory segment persists at least as long as any running process has a handle on that segment. As described with an example scenario in bpo-37754: Another key scenario we expect to work but does not currently:
The SharedMemoryManager provides a flexible means for ensuring cleanup of shared memory segments. The current resource tracker attempts to treat shared memory segments as equivalent to semaphore references, which is too narrow of an interpretation. As such, the current resource tracker should not be attempting to enforce cleanup of shared memory segments because it breaks expected behavior and significantly limits functionality. |
Hi Davin, But, this would still not make Unix's implementation consistent with Windows. I know that you already know this. I am commenting to find out, that what would be the next steps to fix the above inconsistency. You could see my last comment(msg351445) in bpo-37754, where I have listed some ways to implement the above reference counting mechanism. If you could have a look and see which one would be the best way, I would be happy to make a PR for it. |
@davin, could you merge one or the other of the PRs that fix this? Presumably also backport to 3.9 and 3.8 (but that's up to you and the release manager). |
As per Guido's comment (#21516 (comment)), I'm going to use this space to discuss ways to go forward with resource tracking and SharedMemory. Taking inspiration from Vinay (https://bugs.python.org/issue37754#msg351445), I think the simplest and best way forward is to use a small section of the shared memory at the start as a reference counter. Every time a process latches onto a shared memory block, it does and atomic increment to the reference counter. And if it detaches, it does an atomic decrement. This atomic operations are available in C via hardware specific instructions. This would require modifying the Python C code posixshmem.c. It should not be a difficult change. This would then change the SharedMemory API such that a call to I think this would also play nice with the current implementation of the |
I recommend bringing this new proposal up on python-dev or python-ideas, to get more eyeballs on the ideas before attempting implementation. One immediate worry I have is that if the reference counter is maintained in the shared memory segment, every process has to participate, and if a process crashes (segfaults) the shared refcount will be forever wrong and the segment will leak. Distributed GC is hard! |
That's a valid point Guido. But, I guess this can be easily handled by resource tracker. At this current moment resource tracker unlinks shared memory if the process which created it dies without unliking it. Therefore, resource tracker handles cleaning up resources which the respective processes couldn't or didn't do. So, instead of unlinking the shared memory segment, resource tracker can instead decrement the reference count, if the process failed to do so, and if the reference count becomes 0, then unlink the shared memory segment. This approach will ensure that even if the respective processes died unexpectedly, there are no leaks. |
Unless the resource_tracker also dies along with the process. In which case, I'm not sure what there is there to do. I believe the resource_tracker actually spawns a process alongside the process that uses it. So if the parent process seg-faults, the resource_tracker should still be alive. |
Well, the chances of resource tracker dying abruptly are very less because it's thoroughly tested, and there are mechanisms to re-spawn resource_tracker process if you see the code. There is a function called Resource tracker is still alive even if the process for which it was created dies. It also handles cleaning shared semaphores. So, I guess this is something we can rely on for cleaning up things, because at the end of the day that's what it was made for. |
Agreed. |
As suggested by Guido I have floated this solution to python-dev mailing list. |
Agree w/ PR here to remove resource tracker unlinking as a quick fix: #15989 This will at least make the unlink behavior more controllable, which is not the case currently (on mac and linux). Would love to have this merged. |
Based on changes at #15989 I've monkey-patched ----- >8 ----- from multiprocessing import resource_tracker
def remove_shm_from_resource_tracker():
"""Monkey-patch multiprocessing.resource_tracker so SharedMemory won't be tracked
def fix_register(name, rtype):
if rtype == "shared_memory":
return
return resource_tracker._resource_tracker.register(self, name, rtype)
resource_tracker.register = fix_register
def fix_unregister(name, rtype):
if rtype == "shared_memory":
return
return resource_tracker._resource_tracker.unregister(self, name, rtype)
resource_tracker.unregister = fix_unregister
if "shared_memory" in resource_tracker._CLEANUP_FUNCS:
del resource_tracker._CLEANUP_FUNCS["shared_memory"]
----- 8< There's a working example in attached file |
Sometimes a leak is exactly what's wanted, i.e. a standing block of shared memory that allows sharing processes come and go ad libitum. I mention this because I haven't seen anyone mention it explicitly. While turicas's monkeypatch covers the use case in which a manually-named block of shared memory is intended to remain indefinitely, it would be best if future versions of shared_memory allowed for such a use case, too. It shouldn't be necessary to monkeypatch in order to have a standing block of shared memory remain ready and waiting even when nobody's using it. |
On the long run: Maybe this could solve some win32api problems? https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ |
Is the use case included where the shared memory object lives longer than the process that created it and where gc respects that shm shall not be gc'ed? Outline:
If not, then I can add the test case here? |
I've added a post here: https://discuss.python.org/t/multiprocessing-with-shared-memory-potential-fix-for-bug-82300/15012
|
python bug related to python/cpython#82300
python bug related to python/cpython#82300
Context: This is all on Linux (Ubuntu 22.04.1 LTS) so I'm going to avoid talking about Windows Dumb question: Isn't SharedMemory supposed to work if a completely different language does the shm_open()? I do an shm_open() in a zig process and then tried to attach and close a SharedMemory() from Python. The resource tracker then complains with:
and unlinks the shared memory. That's a touch ... surprising. If multiprocessing isn't supposed to handle this case, could somebody point me at a more cooperative Python module? Thanks. |
Both removing the resource tracking as well as introducing the reference counting are backwards incompatible. In the former case, the danger of breaking users' software is much lesser though because:
Only cases 3. and 4. are disruptive to the user, both in trivially fixable ways. If we adopted the solution from GH-23174, the situation would be more tricky because:
Therefore, I think what we should do is the following:
Note that the documentation is already recommending use of SharedMemoryManager and already says that "As a resource for sharing data across processes, shared memory blocks may outlive the original process that created them. When one process no longer needs access to a shared memory block that might still be needed by other processes, the close() method should be called. When a shared memory block is no longer needed by any process, the unlink() method should be called to ensure proper cleanup.", implying that this process is not happening by default. |
Can we please get a "track=True" default on the initializer and fix the semantics on close when "track=False"? This issue has been open almost 4 years. I commented on it almost a year ago. Adding "track=True" in the init and having those of us who don't want tracking set it to False is:
Thanks. |
It looks like no core dev exists who feels confident they understand this API well enough to change this API without accidentally breaking existing code. If someone here is still listening who can submit an implementation, please go ahead! It's much easier as a core dev to find time for a code review than to find time to implement an idea from scratch (even if the idea is about changing existing code). |
@pan324 Thanks for putting in the PR. You saved me the time and effort. Let me know if there is something you need me to do. @gvanrossum Thanks for breaking the deadlock on this. What would be an appropriate timeframe to start jumping up and down about this not getting reviewed and committed? |
Start jumping when alpha 2 is released without this. |
@gvanrossum You said I should start jumping up and down when Alpha 2 is released without this. Alpha 2 has been released without this. I'm jumping up and down. 😄 |
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 <lukasz@langa.pl> Co-authored-by: Guido van Rossum <gvanrossum@gmail.com> Co-authored-by: Antoine Pitrou <pitrou@free.fr> Co-authored-by: Gregory P. Smith <greg@krypto.org>
The PR adding the TODO list (i'll put this in the opening comment up top for visibility):
Given the way POSIX "works" (or doesn't, depending on your point of view) I think this is all we can meaningfully do given the platform behavior. |
Thanks for all the discussion & proposed PRs everybody. |
Thanks Greg. I was out of my depth here… |
…ython#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 <lukasz@langa.pl> Co-authored-by: Guido van Rossum <gvanrossum@gmail.com> Co-authored-by: Antoine Pitrou <pitrou@free.fr> Co-authored-by: Gregory P. Smith <greg@krypto.org>
Summary: Fix resource tracker destroys shared memory segments when other processes should still have valid access Upstream issue: python/cpython#82300 Upstream PR: python/cpython@81ee026 Reviewed By: itamaro Differential Revision: D58048029 fbshipit-source-id: 087e18caea2a106a1d4d30eec102fcf2c919cb7b
Glad to see this get resolved. For a backward-compatible fix, I have found that the below works well in case anybody needs this in earlier Python versions: import sys
import threading
from multiprocessing import resource_tracker as _mprt
from multiprocessing import shared_memory as _mpshm
if sys.version_info >= (3, 13):
SharedMemory = _mpshm.SharedMemory
else:
class SharedMemory(_mpshm.SharedMemory):
__lock = threading.Lock()
def __init__(
self, name: str | None = None, create: bool = False,
size: int = 0, *, track: bool = True
) -> None:
self._track = track
# if tracking, normal init will suffice
if track:
return super().__init__(name=name, create=create, size=size)
# lock so that other threads don't attempt to use the
# register function during this time
with self.__lock:
# temporarily disable registration during initialization
orig_register = _mprt.register
_mprt.register = self.__tmp_register
# initialize; ensure original register function is
# re-instated
try:
super().__init__(name=name, create=create, size=size)
finally:
_mprt.register = orig_register
@staticmethod
def __tmp_register(*args, **kwargs) -> None:
return
def unlink(self) -> None:
if _mpshm._USE_POSIX and self._name:
_mpshm._posixshmem.shm_unlink(self._name)
if self._track:
_mprt.unregister(self._name, "shared_memory") This is likely slightly less efficient than simply copying the 3.13 version of |
…ython#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 <lukasz@langa.pl> Co-authored-by: Guido van Rossum <gvanrossum@gmail.com> Co-authored-by: Antoine Pitrou <pitrou@free.fr> Co-authored-by: Gregory P. Smith <greg@krypto.org>
See: python/cpython#82300 the patch overload 'register'&'register' of 'resource_tracker'
Includes backported SharedMemory from 3.13 python/cpython#82300 (comment)
Includes backported SharedMemory from 3.13 python/cpython#82300 (comment)
Includes backported SharedMemory from 3.13 python/cpython#82300 (comment)
my application is using ShareableList() which makes use of SharedMemory() => shared_memory.py .. actually I was glad to see that the track parameter got added to SharedMemory() as of 3.13 onwwards, but I was wondering why this track parameter was not added to ShareableList() also ... doing this will help standalone client processes to close without destroying the ShareableList() of the server process. `class ShareableList:
` |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
TODO list:
track=True
parameter to SharedMemory (gh-82300: Add track parameter to shared memory #110778 did this) so it can be disabled by the user.Linked PRs
The text was updated successfully, but these errors were encountered: