Skip to content

Not able to start an instantiated Thread in a child process through multiprocessing.Process in python 3.13 #134381

Description

@anadrianmanrique

Bug report

Bug description:

I'm experiencing an issue specifically in python 3.13 regarding Threads in the context of multiprocessing.
The following code is working in python < 3.13

from multiprocessing import Process
from threading import Thread
import time
class MyThread(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.__data = ''

    def run(self):
        print("Hi from thread")
        print(self.__data)


class Aclass():
    def __init__(self):
        self._t = MyThread()
        self._t.daemon = True

    def start(self):
        self._t.start()
        print("thread started")

if __name__ == '__main__':
    t = Aclass()
    p = Process(target = t.start )
    p.start()
    time.sleep(2)

After executing the above in python 3.13 I get this output:

 File "/usr/lib/python3.13/multiprocessing/process.py", line 313, in _bootstrap
    self.run()
    ~~~~~~~~^^
  File "/usr/lib/python3.13/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/kali/impacket/master/impacket/tests/SMB_RPC/test_bug_process_thread.py", line 18, in start
    self._t.start()
    ~~~~~~~~~~~~~^^
  File "/usr/lib/python3.13/threading.py", line 973, in start
    _start_joinable_thread(self._bootstrap, handle=self._handle,
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                           daemon=self.daemon)
                           ^^^^^^^^^^^^^^^^^^^
RuntimeError: thread already started

I managed to workaround this by delaying the Thread instance initialization, moving the Thread.init() call to start() method in the derived class, so everything gets executed in the context of the child process.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Linked PRs

Activity

  1. zangjiucheng commented on May 21, 2025

    @zangjiucheng
    Contributor

    Hi, I am a beginner contributor to the CPython project, I scan your issue and I believe this related to following function,

    Modules/_threadmodule.c

    void
    _PyThread_AfterFork(struct _pythread_runtime_state *state)
    {
        // gh-115035: We mark ThreadHandles as not joinable early in the child's
        // after-fork handler. We do this before calling any Python code to ensure
        // that it happens before any ThreadHandles are deallocated, such as by a
        // GC cycle.
        PyThread_ident_t current = PyThread_get_thread_ident_ex();
    
        struct llist_node *node;
        llist_for_each_safe(node, &state->handles) {
            ThreadHandle *handle = llist_data(node, ThreadHandle, node);
            if (handle->ident == current) {
                continue;
            }
    
            // Mark all threads as done. Any attempts to join or detach the
            // underlying OS thread (if any) could crash. We are the only thread;
            // it's safe to set this non-atomically.
    
            /* Issue Here, it will set `handle->state` to THREAD_HANDLE_DONE */
            // handle->state = THREAD_HANDLE_DONE;
    
            handle->once = (_PyOnceFlag){_Py_ONCE_INITIALIZED};
            handle->mutex = (PyMutex){_Py_UNLOCKED};
            _PyEvent_Notify(&handle->thread_is_exiting);
            llist_remove(node);
            remove_from_shutdown_handles(handle);
        }
    }

    There is a related PR to this topic: gh-114271

    If I've made any mistakes, please point them out and I'll continue to explore the issue.

  2. ZeroIntensity commented on May 21, 2025

    @ZeroIntensity
    Member

    The problem is that _PyThread_AfterFork assumes all Thread objects have started so it can remove them. This line is the problem:

    handle->state = THREAD_HANDLE_DONE;

    We need to ignore threads that haven't started. That said, doing this sort of thing is incredibly weird. What's the use-case for creating a Thread in a parent process, but then starting it in a child process?

  3. added
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    on May 21, 2025
  4. anadrianmanrique commented on May 21, 2025

    @anadrianmanrique
    Author

    In my particular scenario, I'm running tests in client/server project. Test code for the client executes server code in a Process instance, as a precondition. Some parts of the server are implemented as Thread subclasses.

  5. zangjiucheng commented on May 22, 2025

    @zangjiucheng
    Contributor

    Hi everyone, I was thinking about this today. Based on an idea from @ZeroIntensity. (It's probably a very basic implement), since this is a thread that hasn't been started yet, I can't think of any harm in keeping it. (The only trouble could be we will need another GC cycle to cleanup the dead thread)

    void
    _PyThread_AfterFork(struct _pythread_runtime_state *state)
    {
        // gh-115035: We mark ThreadHandles as not joinable early in the child's
        // after-fork handler. We do this before calling any Python code to ensure
        // that it happens before any ThreadHandles are deallocated, such as by a
        // GC cycle.
        PyThread_ident_t current = PyThread_get_thread_ident_ex();
    
        struct llist_node *node;
        llist_for_each_safe(node, &state->handles) {
            ThreadHandle *handle = llist_data(node, ThreadHandle, node);
            if (handle->ident == current) {
                continue;
            }
    
    +      // keep not_start handles – they are safe to start in the child
    +      if (handle->state == THREAD_HANDLE_NOT_STARTED){
    +          continue;
    +      }
    
            // Mark all threads as done. Any attempts to join or detach the
            // underlying OS thread (if any) could crash. We are the only thread;
            // it's safe to set this non-atomically.
            handle->state = THREAD_HANDLE_DONE;
            handle->once = (_PyOnceFlag){_Py_ONCE_INITIALIZED};
            handle->mutex = (PyMutex){_Py_UNLOCKED};
            _PyEvent_Notify(&handle->thread_is_exiting);
            llist_remove(node);
            remove_from_shutdown_handles(handle);
        }
    }
  6. ZeroIntensity commented on May 22, 2025

    @ZeroIntensity
    Member

    Sounds reasonable to me.

  7. zangjiucheng commented on May 22, 2025

    @zangjiucheng
    Contributor

    Sounds reasonable to me.

    Hi there, I wish to start draft a PR for this issue. Should I target to branch main or 3.13, I did more test on 3.14 / main branch. It seems another issue pop up. TypeError: cannot pickle '_thread._ThreadHandle' object.

    This only fix the issue on 3.13 now, but I will also keep observe more detail about the new issue on 3.14 and main.

    Thanks for your help @ZeroIntensity @anadrianmanrique

  8. ZeroIntensity commented on May 22, 2025

    @ZeroIntensity
    Member

    Put up a PR for the main branch. If we decide to fix this for older versions, we can automatically backport the fix.

  9. zangjiucheng commented on May 22, 2025

    @zangjiucheng
    Contributor

    Done, thanks so much :)

  10. anadrianmanrique commented on May 22, 2025

    @anadrianmanrique
    Author

    Thank you all for the quick response/fix

  11. colesbury commented on May 22, 2025

    @colesbury
    Contributor

    This pattern will only work with "fork", not "spawn" because you can't pickle a Thread. In 3.13 3.14, the default start method switched from "fork" to "spawn", so this test case won't work as is. You'd need something like multiprocessing.set_start_method("fork") in the if __name__ == '__main__' in addition to the changes proposed by @zangjiucheng. It also will not work on Windows, which doesn't support "fork".

  12. zangjiucheng commented on May 22, 2025

    @zangjiucheng
    Contributor

    @colesbury Thanks a bunch! I was also wondering why this crashed. I learned something new, thanks!

  13. added a commit that references this issue on May 23, 2025
  14. added 2 commits that reference this issue on May 23, 2025
  15. added 2 commits that reference this issue on May 23, 2025
  16. added a commit that references this issue on Jul 12, 2025
  17. added a commit that references this issue on Aug 4, 2025
  18. added a commit that references this issue on Jun 2, 2026
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

    3.13only security fixes3.14bugs and security fixes3.15bugs and security fixesextension-modulesC modules in the Modules dirtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions