-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-83856: Honor atexit for all multiprocessing start methods #114279
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
gh-83856: Honor atexit for all multiprocessing start methods #114279
Conversation
@pablogsal as you are the original assignee for the issue, could you take the first look at this? Thanks. |
I think that first we need some agreement among core devs if this is a good idea at all. I agree that this is indeed more consistent and desirable in a lot of cases but the change in behaviour may unexpectedly affect many systems that rely on the fact that So before we can weight on the patch we need to collect our thoughts regarding this in the issue. I will ping some other core devs there to see what they think |
I'm afraid of changing this as there's probably some software out there that relies on the current behavior. Common Unix "wisdom" has always recommended to call a light-weight exit function such as What we could probably do is add a new We could also be more selective and distinguish Another thing: regardless of whether atexit handlers are inherited from the parent, atexit handlers registered in the child should certainly be executed. |
I definitely agree that we should have more discussion about this behavior, but we should be clear about what's the new behavior first. First of all, the child process will STILL use The atexit handlers from parent is NOT inherited. They are cleaned up immediately in So, the only major thing changed, is the callbacks registered by users with |
My gut feeling is that a change like this is desirable. The inconsistency and odd behavioral corners this changes feel good to make consistent. It seems awkward that any user code executed within a child process that calls an atexit.register API would not see it called when the process it ran in exits. Especially based on the multiprocessing start method (which today also happens to means "based on the platform's default"). That said, I don't expect most code run via multiprocessing to be registering atexit handlers. Which is presumably why the status quo behavior effectively went unnoticed for so long. Similarly to the other issue linked to regarding the That we have two behaviors today, that are both regularly seen by cross platform code, suggests that it is infrequent for code to specifically care. So simplifying our story to make this consistent across start methods feels good. While we could offer yet another API flag to let users control which atexit behavior their multiprocessing using application wants, I don't think that is something we should be happy to provide and maintain. The same goes for extending atexit to have a concept of if the actions happen in forked children or not. We've lived so long without anyone requesting that, I'd rather us not add and maintain such a thing for the future. For user code that calls |
Okay, thanks for the explanation. I agree that this would be an improvement. |
Hi @gpshead, I think we had a discussion above involving a couple of core devs. Should we try to proceed on this? To be honest, code wise, this is not even a very huge change. The structure stays almost the same, the only thing added is the atexit handler. |
A friendly ping for @gpshead , as this PR has been stuck for a month while the code is ready for review. |
Lib/test/test_atexit.py
Outdated
@@ -46,6 +48,43 @@ def test_atexit_instances(self): | |||
self.assertEqual(res.out.decode().splitlines(), ["atexit2", "atexit1"]) | |||
self.assertFalse(res.err) | |||
|
|||
def test_multiprocessing(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a little awkward to have logic specific to multiprocessing start methods on different platforms in test_atexit.py. I think this test would be better off within something like _test_multprocessing.py
which is turned into multiple separate test suites per start method based upon what the platform supports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the test to _test_multiprocessing.py
Hi @gpshead , I updated the tests as requested. Is there anything I should do on this PR? Thanks! |
Thanks! I think this is a good change, as captured by the discussion above. Merging. If anyone runs into issues with it during the 3.13 beta period we can reconsider. |
…ythonGH-114279) Use atexit for all multiprocessing start methods to cleanup. See the pythonGH-114279 PR discussion and related issue for details as to why.
For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment)
For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment)
For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment)
sagemathgh-39201: Fix `spyx_tmp()` cleanup. For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment) In addition, use `sage_` prefix for the main temporary directory. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. URL: sagemath#39201 Reported by: Gonzalo Tornaría Reviewer(s): Antonio Rojas, Gonzalo Tornaría
sagemathgh-39201: Fix `spyx_tmp()` cleanup. For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment) In addition, use `sage_` prefix for the main temporary directory. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. URL: sagemath#39201 Reported by: Gonzalo Tornaría Reviewer(s): Antonio Rojas, Gonzalo Tornaría
sagemathgh-39201: Fix `spyx_tmp()` cleanup. For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment) In addition, use `sage_` prefix for the main temporary directory. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. URL: sagemath#39201 Reported by: Gonzalo Tornaría Reviewer(s): Antonio Rojas, Gonzalo Tornaría
sagemathgh-39201: Fix `spyx_tmp()` cleanup. For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment) In addition, use `sage_` prefix for the main temporary directory. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. URL: sagemath#39201 Reported by: Gonzalo Tornaría Reviewer(s): Antonio Rojas, Gonzalo Tornaría
sagemathgh-39201: Fix `spyx_tmp()` cleanup. For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment) In addition, use `sage_` prefix for the main temporary directory. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. URL: sagemath#39201 Reported by: Gonzalo Tornaría Reviewer(s): Antonio Rojas, Gonzalo Tornaría
sagemathgh-39201: Fix `spyx_tmp()` cleanup. For some reason, a new behaviour of python 3.13 [0] causes the `TemporaryDirectory()` in `sage.misc.temporary_file.spyx_tmp()` to be deleted on child exit, which causes trouble with parallel doctesting [1]. We rewrite `spyx_tmp()` using `tmp_dir()`, which doesn't have this problem, see [2]. [0] python/cpython#114279 [1] sagemath#39188 (comment) [2] sagemath#39188 (comment) In addition, use `sage_` prefix for the main temporary directory. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. URL: sagemath#39201 Reported by: Gonzalo Tornaría Reviewer(s): Antonio Rojas, Gonzalo Tornaría
Now that Python 3.13 is released, did this fully make it in? I don't see a reference in the docs (ex. a "Changed in version 3.13" here) and when I run the example shared provided in #83856, I only see one cleanup, not 2 as expected. I've found one other person who has looked for this fix in Python 3.13 but is not seeing it on their machine: pydantic/logfire#779 I'm using Python 3.13 (and also tried 3.14 alpha 5) on Ubuntu 24.04, in a VM with a Windows host if that matters.
|
@camlee yes this made it to 3.13. Not everything will be announced in whatsnew, this is a relatively small fix rather than a new feature so it's not mentioned. The expected behavior of the example given in #83856 is not two cleanups, it's one. You can read the discussion above - the child process will not inherit the registered atexit callback from their parents - that would be a huge change and potentially break a lot of code out there. The only thing this PR changes, is that the atexit functions registered in the child process will be executed normally, instead of being ignored. |
Thanks @gaogaotiantian! That makes perfect sense now. I figured I was probably looking for the change the wrong way. For those confused like I was, ex. @alexmojaki, here's the same example but modified to register the atexit function in each process individually and this produces the desired two cleanups on Python 3.13 but just one on Python 3.12 showing that the issue was indeed fixed. from multiprocessing import Process, set_start_method
import time
import os
import atexit
def cleanup():
print(f"cleanup {os.getpid()}")
def run():
atexit.register(cleanup) # Registering cleanup for the child process
time.sleep(0.1)
print(f"process done {os.getpid()}")
# atexit._run_exitfuncs()
if __name__ == "__main__":
atexit.register(cleanup) # Registering cleanup for the parent process
set_start_method("fork")
process = Process(target=run)
process.start()
process.join()
print("app finished") In terms of docs, I agree this is pretty small and doesn't need mention in something like what's new. What I was thinking of is something on the atexit page clarifying some of this behavior related to fork, roughly like this: camlee@8b0e922 Thanks again @gaogaotiantian for your very clear and prompt reply! :) |
Currently,
atexit
does not work forfork
andforkserver
methods formultiprocessing
, because multiprocessing kills the forked process withos._exit()
. This causes a couple of issues:atexit
does not work, which is unexpected and undocumented.fork
andforkserver
has a different behavior thanspawn
, which is pretty badspawn
, the cleanup order will be messed up, becauseutil._exit_function
is explicitly called inProcess.bootstrap
, andatexit
callbacks will all be called after it. (See Process _bootstrap calls _exit_function #114220)I'll have to admit that this is not the safest patch, it slightly changes the cleanup routine for all multiprocessing code. However, I still believe the benefit is worth the risk because this results in a consistent behavior.