Skip to content

Lingering Subinterpreter Subthread Keeps Program From Exiting #113146

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

Closed
ericsnowcurrently opened this issue Dec 14, 2023 · 1 comment
Closed
Labels
3.12 only security fixes 3.13 bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-subinterpreters type-bug An unexpected behavior, bug, or error

Comments

@ericsnowcurrently
Copy link
Member

ericsnowcurrently commented Dec 14, 2023

Bug report

Steps:

  1. create a subinterpreter
  2. in it, start a subthread that takes one or more seconds to finish
  3. wait for that subthread to finish

One would expect execution to flow as follows:

main interpreter
main thread
subinterpreter
main thread
subinterpreter
subthread
1 runtime init
2 run script
3 create subinterpreter
4 init
5 run code
6 start subthread run code
7 exit script (run code)
8 begin runtime fini (run code)
9 begin fini (run code)
10 wait for subthread (run code)
11 (wait for subthread) done
12 finish fini
13 finish fini
14 exit

However, that isn't what's happening. Instead, at some point the subthread stops executing (without error) and the process effectively hangs (but can be stopped with Ctrl-C).

Script:
print('START: thread 0 (interp 0) - executing')
import atexit
import threading
from test.support import interpreters


def notify_before_thread_shutdown():
    print('START: thread 0 (interp 0) - waiting for non-daemon threads')
threading._register_atexit(notify_before_thread_shutdown)

def notify_after_thread_shutdown():
    print('END:   thread 0 (interp 0) - waiting for non-daemon threads')
atexit.register(notify_after_thread_shutdown)

interp = interpreters.create()

def task():
    print('START: thread 1 (interp 0) - executing')
    interp.exec_sync("""if True:
        print('START: thread 1 (interp 1) - executing')
        import atexit
        import threading

        def notify_before_thread_shutdown():
            print('START: thread 0 (interp 1) - waiting for non-daemon threads')
        threading._register_atexit(notify_before_thread_shutdown)

        def notify_after_thread_shutdown():
            print('END:   thread 0 (interp 1) - waiting for non-daemon threads')
        atexit.register(notify_after_thread_shutdown)

        def task():
            print('START: thread 2 (interp 1) - executing')
            import time
            for i in range(1, 11):
                if i == 6:
                    print(' ', end='', flush=True)
                print('.', end='', flush=True)
                time.sleep(0.1)
            print(flush=True)
            print('END:   thread 2 (interp 1) - executing')
        t = threading.Thread(target=task)
        t.start()
        # t.join() here makes the problem go away.
        print('END:   thread 1 (interp 1) - executing')
    """)
    print('END:   thread 1 (interp 0) - executing')
t = threading.Thread(target=task)
t.start()
t.join()
# interp.close() here makes the problem go away.
# time.sleep(1) here makes the problem go away.
print('END:   thread 0 (interp 0) - executing')

Expected output:

START: thread 0 (interp 0) - executing
START: thread 1 (interp 0) - executing
START: thread 1 (interp 1) - executing
START: thread 2 (interp 1) - executing
END:   thread 1 (interp 1) - executing
END:   thread 1 (interp 0) - executing
END:   thread 0 (interp 0) - executing
START: thread 0 (interp 0) - waiting for non-daemon threads
END:   thread 0 (interp 0) - waiting for non-daemon threads
START: thread 0 (interp 1) - waiting for non-daemon threads
..... .....
END:   thread 2 (interp 1) - executing
END:   thread 0 (interp 1) - waiting for non-daemon threads

Expected output:

START: thread 0 (interp 0) - executing
START: thread 1 (interp 0) - executing
START: thread 1 (interp 1) - executing
START: thread 2 (interp 1) - executing
END:   thread 1 (interp 1) - executing
END:   thread 1 (interp 0) - executing
END:   thread 0 (interp 0) - executing
START: thread 0 (interp 0) - waiting for non-daemon threads
END:   thread 0 (interp 0) - waiting for non-daemon threads
START: thread 0 (interp 1) - waiting for non-daemon threads

I'm fairly sure I know what the problem is: our hack for dealing with daemon threads. Currently, between steps 8 and 9 above, we mark the runtime as finalizing. Any threads (regardless of interpreter) are more or less immediately killed.

However, as far as the threading module is concerned, they are still valid and running and will never stop. This isn't a problem for the main interpreter because at that point it already waited for it's own non-daemon threads to finish.

This is a problem for subinterpreters because they don't wait for their non-daemon threads to finish until later. Thus we end up waiting forever at step 10.

The solution? Immediately after waiting for non-daemon threads in step 8, finalize all subinterpreters. Alternately, we could make sure we're only killing daemon threads with our hack (e.g. don't use a global "finalizing" check).

@ericsnowcurrently ericsnowcurrently added type-bug An unexpected behavior, bug, or error interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-subinterpreters 3.12 only security fixes 3.13 bugs and security fixes labels Dec 14, 2023
@ZeroIntensity
Copy link
Member

Duplicate of #128639.

@github-project-automation github-project-automation bot moved this from Todo to Done in Subinterpreters May 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.12 only security fixes 3.13 bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) topic-subinterpreters type-bug An unexpected behavior, bug, or error
Projects
Status: Done
Development

No branches or pull requests

2 participants