python / python/cpython

Thread.start() can hang indefinitely if the new thread fails (MemoryError) during its initialization

Đang mở
#140,746 10 bình luận 4 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

stdlib type-bug
Ngôn ngữ chính
Python
Star
77.2k
Fork
35.9k
Chỉ số merge pull request
Chỉ số pull request đang chờ

Mô tả

Bug report

Bug description:

There is a race condition in the threading module where a parent thread calling Thread.start() can wait forever if the newly created thread crashes with a MemoryError during its internal bootstrap process.

Case Explanation

In case we have a "Serving Thread" that creates threads on demand (e.g., for each HTTP request):

  • When this Serving Thread calls Thread.start(), the OS-level thread (pthread_create()^1 - Linux) is successfully created. The parent thread then waits for the new thread to signal that it has started correctly by calling self._started.wait()^2.
  • The new thread starts, but before it can signal the parent thread (Serving Thread) that it is alive with self._started.set()^3 it encounters a MemoryError.
  • This MemoryError can occur at the C level during the PyObject_Call to the _bootstrap method or inside the _bootstrap_inner method before _started.set() is reached, often due to memory pressure from other threads (or a heap limit being reached).
  • This exception is caught by the C-level entry point thread_run(), which calls _PyErr_WriteUnraisableMsg^4 and prints "Exception ignored in thread started by: ..."

The new thread then exits without ever signaling the _started event, and the parent thread waits indefinitely on the _started.wait().
This also leaves the threading module in an inconsistent state, as the "zombie" thread object may not be correctly cleaned up from the _limbo dict.

How to Reproduce

This has been observed in high-concurrency server applications under heavy, sustained load, where heap memory can be rapidly consumed and exhausted by concurrent threads ^5.
This is a race condition that is difficult to reproduce reliably, as it requires triggering a MemoryError at a specific moment.

I found a (deterministic?) way to reproduce the issue by restricting the heap memory until it reaches a threshold where we can start a new thread, but this new thread won't get enough memory for its initialization.

On some machines (and depending of Python versions), it is sometimes necessary to tweak HARD_LIMIT_START / LIMIT_REDUCTION (I reproduced it on Ubuntu based machine with Python 3.11/3.12/3.13/3.14)

import resource
import threading
import gc

def handler():
    pass

def serving():
    # These should be tweak (depending of Python version + system)
    HARD_LIMIT_START = 30_000_000
    LIMIT_REDUCTION = 5_000

    for _ in range(500_000):
        gc.collect(2)  # Force getting back memory: seems to increase the determinism of the script

        # Limit the heap size available for this process
        resource.setrlimit(resource.RLIMIT_DATA, (HARD_LIMIT_START, HARD_LIMIT_START * 2))
        try:
            handler_thread = threading.Thread(target=handler)
            print(f'Start Thread: {handler_thread} - Heap size limit : {HARD_LIMIT_START}')
            handler_thread.start()
            handler_thread.join()
            HARD_LIMIT_START -= LIMIT_REDUCTION
        except RuntimeError as r:  # If Python refused to launch a new Thread
            print(f'RuntimeError: {r} - Cannot start the thread at all => error not detected.')
            return

serving_thread = threading.Thread(target=serving)
serving_thread.start()
serving_thread.join()
Expected Behavior

I am not sure if this is an "accepted" limitation of (CPython) Thread or not. IMO, Thread.start() shouldn't hang indefinitely if the low-level thread is dead.

I didn't take the time to try to fix it yet (if possible). I would prefer to get your opinions on this first.

CPython versions tested on:

3.12, 3.13, 3.14

Operating systems tested on:

Linux

Linked PRs
  • gh-140799
  • gh-144750
  • gh-153776

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu với Lib/threading.py, quanh Thread.start() và _bootstrap_inner, sau đó kiểm tra entry point được tham chiếu trong Modules/_threadmodule.c và phần triển khai pthread trong Python/thread_pthread.h. Chạy trình tái hiện giới hạn tài nguyên Linux được cung cấp trên các phiên bản CPython đã liệt kê. Công việc được hoàn tất khi Thread.start() không chờ vô thời hạn khi khởi tạo thất bại, với coverage hồi quy cho nhánh lỗi.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
operating-systems
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Đình trệ
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
25/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.