Skip to content

Thread.join() cannot be interrupted by Ctrl+C on Windows #139689

Description

@james-oldfield-inex

Bug report

Bug description:

The following short program will always run for 10 seconds on Windows, no matter how quickly you press ctrl+C.

from threading import Thread
import time

t = Thread(target=time.sleep, args=(10,))
t.start()

try:
    print("Joining ...")
    t.join()
    print("Joined without exception")
except KeyboardInterrupt:
    print("Keyboard interrupt")

The program does print "Keyboard interrupt", so the signal handler does actually get run and the exception gets thrown. It just doesn't interrupt the join. Indeed, if the thread function is changed to one that runs forever then this code would wait forever too.

The following expanded version, with a custom signal handler, shows that the signal handler is not run until the join() completes. (As opposed to the possibility that it runs straight away, but then the join() wait resumes before raising the exception. Unlikely I know.)

Version with custom signal handler
import signal
from threading import Thread
import time

start_time = time.monotonic()
def pr(s):
    print(f"{time.monotonic() - start_time:.2f} {s}")

def sig_handler(signal, frame):
    pr("Ctrl-C")
    raise KeyboardInterrupt

signal.signal(signal.SIGINT, sig_handler)

t = Thread(target=time.sleep, args=(10,))
t.start()

try:
    pr("Joining ...")
    t.join()
    pr("Joined without exception")
except KeyboardInterrupt:
    pr("Keyboard interrupt")

If ctrl+c is pressed within 10 seconds then this prints:

0.00 Joining ...
10.00 Ctrl-C
10.00 Keyboard interrupt

Related issues:

One of the comments on #66021 is about this Thread.join() rather than the real topic of that issue, but they are told "This is a different issue: bpo-29971. Currently, threading.Lock.acquire() cannot be interrupted by CTRL+C." This suggests to me that #74157 is very related to Thread.join() not being interruptable. But it's not exactly the same ... because it's marked as fixed and this issue still persists.

CPython versions tested on:

3.10, 3.12

Operating systems tested on:

Windows

Activity

  1. picnixz commented on Oct 7, 2025

    @picnixz
    Member

    I think this is unfortunately something that is specific to Windows. On Linux, I would have a hang after calling Ctrl+C because I would then move to thread.shutdown(), which I would once again need to Ctrl+C because this still tries to join it.

    As for the signal, IIRC, it's only delivered to the main thread. That's the POSIX standard (and somehow documented at the end of https://docs.python.org/3/library/_thread.html#module-_thread).

    I think most of the weird behavior you can see on Windows is actually dependent on the platform itself and not on CPython. Interruptible locks (that are used to implement join()) are not always supported (see https://mail.python.org/pipermail/python-dev/2017-August/148805.html).

    cc @ZeroIntensity

  2. ZeroIntensity commented on Oct 7, 2025

    @ZeroIntensity
    Member

    which I would once again need to Ctrl+C because this still tries to join it.

    Hm, this doesn't sound right. If you interrupt threading._shutdown, the interpreter won't attempt to join remaining threads. In fact, I broke this behavior recently and had to fix it.

    Interruptible locks (that are used to implement join()) are not always supported

    We don't use interruptible locks to implement Thread.join(). Instead, we have a loop with PyEvent_WaitTimed that calls Py_MakePendingCalls every millisecond or something like that. I think that should work on Windows.

    I wonder if this is something we fixed with the introduction of free-threading in 3.13. @james-oldfield-inex Does this still occur on 3.13 (or 3.14, since that's getting released today)?

  3. james-oldfield-inex commented on Oct 7, 2025

    @james-oldfield-inex
    Author

    Oops, I've found there was an issue that this is an exact duplicate of:

    Guido closed that saying "This is because the regular acquire() method on a basic lock cannot be interrupted. That's unlikely to go away, so you'll just have to live with this." But as we've seen in #89464, that has now gone away, so presumably this is now fixable (and presumably this is why one of those comments equated this issue with the acquire() one). So I think that either this issue should stay open, or be merged with #41740 and that be reopened.


    @ZeroIntensity

    We don't use interruptible locks to implement Thread.join().

    Hmm, perhaps this is a change since Guido's comment? I guess that's why this didn't magically go away when #89464 was fixed, as at least one person expected. But I suppose it proves it can be done? I'm tempted to go on to ask why you don't use interruptible locks to implement Thread.join() but the reality is I don't know the internals well enough to do anything with that information.

    I will try out some other Python versions soon.


    @picnixz

    On Linux, I would have a hang after calling Ctrl+C because I would then move to thread.shutdown(), ... As for the signal, IIRC, it's only delivered to the main thread.

    I think you're reading too much into the example code. I was including just minimal code to demonstrate the behaviour. Yes, the toy program would go on to hang anyway if the join() were successfully interrupted. In the real program that this is based upon, after thread.join() is interrupted I would do something application-specific to stop my thread (close the socket it is using) then do another thread.join() which should now return fairly promptly. And I understand that signals can only happen in the main thread (yes that's true on Windows too).

  4. picnixz commented on Oct 7, 2025

    @picnixz
    Member

    @ZeroIntensity

    Hm, this doesn't sound right. If you interrupt threading._shutdown

    Maybe I wasn't clear. I meant

    from threading import Thread
    import time
    
    def p():
        print(1)
        time.sleep(3)
        print(3)
    
    t = Thread(target=p)
    t.start()
    t.join()
    $ ./python test.py
    1
    ^CTraceback (most recent call last):
      File "/[...]/cpython/test.py", line 48, in <module>
        t.join()
        ~~~~~~^^
      File "/[...]/cpython/Lib/threading.py", line 1125, in join
        self._os_thread_handle.join(timeout)
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
    KeyboardInterrupt
    3

    Indeed, if I hit Ctrl+C again I get:

    1
    ^CTraceback (most recent call last):
      File "/[...]/cpython/test.py", line 48, in <module>
        t.join()
        ~~~~~~^^
      File "/[...]/cpython/Lib/threading.py", line 1125, in join
        self._os_thread_handle.join(timeout)
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
    KeyboardInterrupt
    ^CException ignored on threading shutdown:
    Traceback (most recent call last):
      File "/[...]/cpython/Lib/threading.py", line 1576, in _shutdown
        _thread_shutdown()
        ~~~~~~~~~~~~~~~~^^
    KeyboardInterrupt

    So yes, _thread_shutdown() doesn't try to join again.

  5. removed
    pendingThe issue will be closed if no feedback is provided
    on Oct 7, 2025
  6. james-oldfield-inex commented on Oct 7, 2025

    @james-oldfield-inex
    Author

    Update:

    • I tried on Python 3.13 (installed with Anaconda) and it still has the same bug in the example
    • I tried python.exe from Python 3.14rc3 installer from the website and this is now fixed! join() gets interrupted immediately
    • I tried python3.14t.exe (free-threaded binary) from Python 3.14rc3 and again it's fixed!

    These are all 64 bit binaries by the way.

    Is there something plausible in 3.14 that would have fixed this? If so I guess this can just be closed.

  7. ZeroIntensity commented on Oct 7, 2025

    @ZeroIntensity
    Member

    We made a bunch of changes to _thread in 3.14 for free-threading, so someone probably fixed this along the way. I think we should just close this since it's fixed and probably not something we want to backport to 3.13.

  8. james-oldfield-inex commented on Oct 7, 2025

    @james-oldfield-inex
    Author

    @picnixz About your last post - this is correct behaviour, as these are not daemon threads so Python is documented as waiting for them to exit even after the main script has completed. If you set daemon=True then you'll find the whole program stops immediately when you press ctrl+c (if you're running on a platform/version where Thread.join() is interruptible), which again is correct.

  9. james-oldfield-inex commented on Oct 7, 2025

    @james-oldfield-inex
    Author

    As per @ZeroIntensity closing this issue as fixed in 3.14 (to be released later today - now that is a very fast resolution 😄 )

  10. kumaraditya303 commented on Oct 7, 2025

    @kumaraditya303
    Contributor

    For reference this was done in #125546 following #125110.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions