Skip to content

ProcessPoolExecutor worker processes stay alive after parent process is killed #111873

Open
@zpincus

Description

@zpincus

Bug report

Bug description:

If a Python process using concurrent.futures.ProcessPoolExecutor dies in a way that prevents cleanup (e.g. a kill signal), the child processes do not exit. In the case of a forking multiprocessing context, this can lead to significant resource leaks (as the forked children inherit the parent resources but do not exit).

Here is a minimal reproduction:

import multiprocessing
import os
import time
from concurrent import futures

def task(i):
    print('Task', i)
    time.sleep(1)

def queue_and_die():
    print(pid := os.getpid())
    with futures.ProcessPoolExecutor(max_workers=4) as executor:
        for i in range(20):
            executor.submit(task, i)
        time.sleep(1)
        os.kill(pid, 9)

if __name__ == '__main__':
    import sys
    multiprocessing.set_start_method(sys.argv[1])
    queue_and_die()

Then in the terminal, run as e.g. python test.py fork (or use spawn or forkserver; all give the same result) and note the pid on the first line of output. After the parent process dies, use e.g. pgrep -ag <PID> to observe that the worker processes (with the process group ID corresponding to the PID of the parent process) are still alive. kill -9 -<PID> will clean these right up.

This behavior has been noted a few places; e.g. https://stackoverflow.com/questions/71300294/how-to-terminate-pythons-processpoolexecutor-when-parent-process-dies. However, the suggested solution (a thread in the worker process that polls to see if the parent PID is still alive) could fail if the parent PID gets reused. (IIUC the likelihood of this varies by OS...)

A better solution might be to hold on to e.g. the read end of a pipe that the parent has open for writing, and then use poll/select on the child side to determine if the pipe has been closed on the write end (i.e. poll will return that it can be read, but then reads will yield an EOFError or whatnot).

In ProcessPoolExecutor this could be done in a separate thread to periodically poll the pipe, or could be done right before the blocking call to call_queue.get to retrieve the next work item.

CPython versions tested on:

3.10.8, 3.12

Operating systems tested on:

Linux, macOS

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions