-
-
Notifications
You must be signed in to change notification settings - Fork 30.8k
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
importlib.reload
is not thread-safe
#126548
Comments
Quick note: if |
Hmm, how does this affect <3.13? Threads shouldn't be able to execute and trigger the race condition because of the GIL, so this should theoretically only exist on free-threaded builds, which are only available on 3.13+ |
I'm of two minds on this one. First, I think that As for testing, we tend to dislike sleeps since it holds all the testing up. Honestly, the logic behind the lock is simple enough that just passing the test suite as-is should be fine. And I agree with @ZeroIntensity that this doesn't seem to be a concern prior to Python 3.13 (hence why I dropped 3.12 from the effected versions). |
Well, I was able to get the What I am worried about is the error message that can come up on the free-threaded builds:
Anyways, we need some locking around it. @bswck said he would submit a PR, so we can wait on that 😄. In the meantime, I'd be curious to hear if @ncoghlan has any input on this, because she had a thread on discord last week about |
A few thoughts:
I'd be a bit hesitant about the proposed RLock approach because:
|
Odd, I can't reproduce that error on a GILful build at all, even with thousands of threads. I guess my system is just being silly. |
Thanks for the invaluable input.
Makes sense. It is indeed what the docs highlight: This is useful if you have edited the module source file using an external editor and want to try out the new version without leaving the Python interpreter. However, what we could do if we lean toward not making
Yes! That's what I forgot to underline while writing this. It targets every supported version. The behavior we'd expect of I never reproduced cpython/Lib/importlib/__init__.py Lines 108 to 109 in b19d12f
just between these two statements carried out by some other thread: cpython/Lib/importlib/_bootstrap.py Lines 870 to 871 in b19d12f
which is quite a time distance and is much more likely to occur with more threads involved.
I'd personally enjoy it being thread-safe, but my personal joy doesn't necessarily align to optimal practical implications (potential performance degradation, code complication for a case that's never been reported in now more than 10 years of existence). Thank you @jaraco for hinting me in that matter.
Great point, and a mistake in my initial assumption that the function should "make other threads access the not-yet-reloaded module eagerly, while it is being reloaded by the first thread to have initiated the reload". After all, I'm leaning toward giving up on introducing new lock mechanisms for navigating this and instead documenting the intent that this function has had for years. If this issue gathers interest in the future and proves necessary to make So, what option (1. emphasize REPL-only intent, 2. make thread-safe) would you be more inclined to from a clearly practical point of view? Can you imagine a use case where |
I say just make the docs stronger around what people's expectations should be, but if you want to give it a go to fix and the PR is simple enough without slowing down importing we can at least consider it. |
I will definitely learn a lot from giving a shot to implementing a fix, so let's go! |
Bug report
Bug description:
There are two problems with
importlib.reload
regarding thread safety, and both are race conditions.I naturally assume the function should be thread-safe, consistently with other components of
importlib
.I never ran into someone reloading the same module from different threads, but I can imagine such a scenario happen in a large, self-aware codebase.
Problem 1: Race condition when the same module is reloaded from two threads simultaneously
This guard in
importlib.reload
is probably supposed to do two things:cpython/Lib/importlib/__init__.py
Lines 110 to 112 in 85036c8
_bootstrap._exec
finishes execution), return it immediately.Assuming (2) is what the snippet is supposed to do, I'm getting consistent failures to fulfill that goal.
To reproduce:
Place the above files in the current working directory.
If
python repro.py
(test on whichever supported Python version you wish) outputsrace condition: reloaded again
, the repro script hit a race condition, because the modulereloadee
was reloaded more than once, by separate threads.If it outputs nothing, it's a case where the race condition was missed.
Consequently, running
consistently yields numbers in range 80-90 on my personal machine.
Note
There is no case of module contents being executed in parallel, because
_bootstrap._exec
executes only one module at a time (thanks to_ModuleLockManager
):cpython/Lib/importlib/_bootstrap.py
Lines 845 to 848 in 3d9f9ae
Problem 2: Classic LBYL race condition
Because the current implementation doesn't synchronize
_RELOADING
access, it is easy to see that between those linescpython/Lib/importlib/__init__.py
Lines 110 to 111 in 85036c8
the reloaded module (
_RELOADING[name]
) can disappear from_RELOADING
, in case a different thread executescpython/Lib/importlib/__init__.py
Line 134 in 85036c8
I think you can imagine this being pretty likely to happen either :)
It is nicely explained in the glossary.
But let me know if we still need a repro for that one too.
Suggested solution
A simple
RLock()
(notLock()
, see point 1. in Problem 1 – same thread that invokedreload
can re-run it, and thus should re-enter the lock without being blocked) only around the critical sectioncpython/Lib/importlib/__init__.py
Lines 110 to 112 in 85036c8
should suffice.
Please let me know what you think about that suggestion. I'm wondering how to test this future fix specifically – I tend to like
threading.Barrier
andtime.sleep
to ensure 100% reproducibility of cases, but maybe there's a better way?Expect a PR from me as soon as the decision (fix / document thread-unsafety) is made!
CPython versions tested on:
3.9, 3.10, 3.11, 3.12, 3.13, 3.14, CPython main branch
Operating systems tested on:
Linux, macOS, Windows
The text was updated successfully, but these errors were encountered: