Skip to content

Thread.is_alive should only return False after a thread exits (once it's started) #137017

Closed

Description

@colesbury

Feature or enhancement

Currently, Thread.is_alive will return False shortly after the run() function finishes, even though the underlying OS thread may still be alive.

In #110829, we made Thread.join call the OS join API so that it waits for the OS thread to actually exit. We should apply the same behavior to Thread.is_alive():

  • In some places, we use Thread.is_alive as an alternative to Thread.join. For example, in test_free_threading.test_monitoring, the test exits once all the background threads are no longer "alive", but that may be a little too soon -- the OS threads may still be running, which can lead to false positives in the refleak tests.
  • The pattern for Thread.join(timeout=...) requires calling Thread.is_alive afterwards to differentiate timeouts vs. successful joins, but is_alive() may return False "too soon", while the OS thread is still alive.

(Split off from #136692)

cc @mpage @gpshead

Linked PRs

Activity

  1. added
    type-featureA feature request or enhancement
    stdlibStandard Library Python modules in the Lib/ directory
    on Jul 22, 2025
  2. gpshead commented on Jul 22, 2025

    @gpshead
    Member

    This makes a lot of sense if we can actually get this plumbed right.

  3. deepwzh commented on Jul 30, 2025

    @deepwzh
    Contributor

    Can I try to modifying Thread.is_alive to make it return True when the thread hasn't really exited?

  4. colesbury commented on Jul 30, 2025

    @colesbury
    ContributorAuthor

    I think it's more subtle than that. I don't think there's a way to determine if a thread has really exited without joining it. So you need to check if the thread is about to exit and in that case join.

    Also need to make sure that is_alive on DummyThreads and the MainThread still work appropriately and update the docs.

  5. abdoulrasheed commented on Aug 1, 2025

    @abdoulrasheed
    Contributor

    @colesbury can we update PyThreadHandleObject_is_done in _threadmodule.c to join the thread?

    Edited Diff
     static PyObject *
     PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy)){
         PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
         if (_PyEvent_IsSet(&self->handle->thread_is_exiting)) {
    -        Py_RETURN_TRUE;
    +        if (ThreadHandle_join(self->handle, -1) == 0) {
    +            Py_RETURN_TRUE;
    +        }
         }
         else {
             Py_RETURN_FALSE;
         }
     }
    static PyObject *
    PyThreadHandleObject_is_done(PyObject *op, PyObject *Py_UNUSED(dummy))
    {
        PyThreadHandleObject *self = PyThreadHandleObject_CAST(op);
        if (_PyEvent_IsSet(&self->handle->thread_is_exiting)) {
    +      if (self->handle->ident != PyThread_get_thread_ident()) {
    +          (void)ThreadHandle_join(self->handle, -1);
    +      }
            Py_RETURN_TRUE;
        }
        else {
            Py_RETURN_FALSE;
        }
    }
  6. gpshead commented on Aug 1, 2025

    @gpshead
    Member

    While it ordinarily feels surprising to trigger a potentially blocking join from a boolean .is_alive() method... I don't think that join is blocking in practice in the scenarios where this could be called, it looks like we only set .thread_is_exiting (via PyNotify) in scenarios where we are not doing any further calls that might meaningfully block. (I'm ignoring any potential for malloc to block as irrelevant)

  7. colesbury commented on Aug 1, 2025

    @colesbury
    ContributorAuthor

    @abdoulrasheed, I think it should be enough to do the following, instead of ThreadHandle_join:

    if (_PyEvent_IsSet(&self->handle->thread_is_exiting)) {
        if (_PyOnceFlag_CallOnce(&self->once, join_thread, self) == -1) {
            return -1;
        }
        Py_RETURN_TRUE;
    }

    ... I don't think that join is blocking in practice in the scenarios where this could be called

    Yeah, I agree both that it feels a bit weird and I also don't think it should block in practice.

    We'll need to be careful about the main thread and "dummy" threads (i.e., threads created from C/C++ outside the threading module), since they have different behaviors.

  8. abdoulrasheed commented on Aug 1, 2025

    @abdoulrasheed
    Contributor

    Thanks @colesbury. I'll update and open a PR.

  9. added a commit that references this issue on Aug 1, 2025
  10. abdoulrasheed commented on Aug 1, 2025

    @abdoulrasheed
    Contributor

    @colesbury The function is expected to return a PyObject*. I assume you meant to return NULL and set an appropriate exception, rather than return -1?

    I've updated it to return NULL instead.

  11. added a commit that references this issue on Sep 15, 2025
  12. added 2 commits that reference this issue on Sep 15, 2025
  13. added a commit that references this issue on Sep 15, 2025
  14. added a commit that references this issue on Oct 7, 2025
  15. hugovk commented on Oct 10, 2025

    @hugovk
    Member

    OK to close now the 3.14 backport has been merged?

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

    stdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions