You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python subprocess.Popen启动子进程后脚本挂起不终止的原因与解决

问题:Tkinter父进程调用子进程后程序无限挂起无法终止

单独运行Tkinter父脚本或子进程脚本时都能正常终止,但通过subprocess.Popen调用子进程后,整个程序会无限挂起无法退出。

父脚本代码

import os
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import messagebox
import subprocess

image_folder = r'C:\Users\anton\Pictures\image_editing\CURRENT IMAGE FOLDER' # the folder path of the images to curate
requires_editing_folder = r'C:\Users\anton\Pictures\image_editing\REQUIRES IMAGE EDITING'

files = os.listdir(image_folder)
files_index = 0

root = tk.Tk() # creates a simple GUI window
root.geometry('800x600')

canvas = tk.Canvas(root, width=800, height=600)
canvas.pack(side="left", fill="both", expand=True)

scrollbar_v = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
scrollbar_v.pack(side="right", fill="y")
scrollbar_h = tk.Scrollbar(root, orient="horizontal", command=canvas.xview)
scrollbar_h.pack(side="bottom", fill="x")

canvas.configure(yscrollcommand=scrollbar_v.set, xscrollcommand=scrollbar_h.set)

frame = tk.Frame(canvas)
canvas.create_window((0,0), window=frame, anchor='nw')

label = tk.Label(frame)
label.pack()

zoom_state = False
img = None

# Functions for mousewheel scrolling
def scroll_v(event):
    canvas.yview_scroll(int(-1*(event.delta)), "units")
canvas.bind_all("<MouseWheel>", scroll_v)

def scroll_h(event):
    canvas.xview_scroll(int(-1*(event.delta)), "units")
canvas.bind_all("<Shift-MouseWheel>", scroll_h)

# Functions for keyboard scrolling
def scroll_up(event):
    canvas.yview_scroll(-5, "units")

def scroll_down(event):
    canvas.yview_scroll(5, "units")

def scroll_left(event):
    canvas.xview_scroll(-5, "units")

def scroll_right(event):
    canvas.xview_scroll(5, "units")

def load_next_image(): 
    global img
    global files_index
    if files_index >= len(files):
        messagebox.showinfo("Information", "No more images left")
        root.quit()
    else:
        img_path = os.path.join(image_folder, files[files_index])
        img = Image.open(img_path)
        update_image_display()
        files_index += 1

def delete_current_image(): # deletes the current image and loads the next one
    global files_index
    img_path = os.path.join(image_folder, files[files_index - 1])
    os.remove(img_path)
    files.pop(files_index - 1)
    files_index -= 1
    load_next_image()

def update_image_display(): # toggles zoom in, zoom out
    global img
    if zoom_state:
        tmp = img.resize((img.width, img.height)) # 100% size
    else:
        tmp = img.resize((int(img.width*0.4), int(img.height*0.4))) # 40% size
    photo = ImageTk.PhotoImage(tmp)
    label.config(image=photo)
    label.image = photo

    # Updating scroll region after image is loaded
    root.update()
    canvas.configure(scrollregion=canvas.bbox("all"))

def toggle_zoom(event): # toggles zoom state pt 2
    global zoom_state
    zoom_state = not zoom_state
    update_image_display()

def move_current_image():
    global files_index
    img_path = os.path.join(image_folder, files[files_index - 1])
    new_path = os.path.join(requires_editing_folder, files[files_index - 1])
    os.rename(img_path, new_path)

    subprocess.Popen(["python", "pixelbin-image-transformation-v3.py", files[files_index - 1], requires_editing_folder], 
                     start_new_session=True)

    files.pop(files_index - 1)
    files_index -= 1
    load_next_image()

# Bind the keys to the desired events
root.bind('<Right>', lambda event: load_next_image()) # approves this image, moves to next
root.bind('<Left>', lambda event: delete_current_image()) # deletes this image, as it doesn't meet the criteria
root.bind('<Up>', lambda event: move_current_image()) # moves this image into "requires photoshop editing" holding bay folder
root.bind('e', toggle_zoom) # bounces back between 100% and 40% zoom

# keyboard-based scrolling, for maximum efficiency
root.bind('f', scroll_down)
root.bind('r', scroll_up)
root.bind('d', scroll_left)
root.bind('g', scroll_right)

load_next_image() # start the workflow
root.mainloop() # start the GUI main loop

子进程脚本代码

import sys
import os

# STEP 1 = UPLOAD IMAGE FILE TO PIXELBIN
# DOCUMENTATION FOR API-BASED FILE UPLOADING: https://github.com/pixelbin-dev/pixelbin-python-sdk/blob/main/documentation/platform/ASSETS.md#fileupload

CurrentImageFilename = sys.argv[1] # this will be files[files_index - 1] from the parent script, which gets passed into this script as a variable
CurrentImageFolder = sys.argv[2] # this will be image_folder from the parent script, because this is run as a subprocess
CurrentFilepathFull = os.path.join(CurrentImageFolder, CurrentImageFilename)
FinalImageDownloadFolder = r"C:\Users\anton\Pictures\image_editing\IMAGE EDITING COMPLETED"

import asyncio
from pixelbin import PixelbinClient, PixelbinConfig

config = PixelbinConfig({
    "domain": "https://api.pixelbin.io",
    "apiSecret": "my_api_key_goes_here",
})

pixelbin:PixelbinClient = PixelbinClient(config=config)

try:
    print("Pixelbin image upload in progress...")
    result = pixelbin.assets.fileUpload( # sync method call (there's also an async method, in the above documentation)
        file=open(CurrentFilepathFull, "rb"),
        path="IMAGE_EDITING_FOLDER",
        name=CurrentImageFilename, # uses the unique filename each time since I may be running transform operations in parallel. doing so on identical filenames, before deleting the previous, would absolutely cause timing problems.
        access="public-read",
        tags=["tag1","tag2"],
        metadata={},
        overwrite=True,
        filenameOverride=True)
    print("Image uploaded successfully!")
    # print(result)
except Exception as e:
    print(e)

# Pixelbin automatically modifies filenames like so. I therefore follow their convention so I can later delete the correctly-formatted filename.
FilenameModifiedByPixelbin = CurrentImageFilename.replace('.', '_') 

# STEP 2 = RUN TRANSFORMATION ON SPECIFIED IMAGE
# DOCUMENTATION FOR URL-BASED TRANSFORMATIONS: https://www.pixelbin.io/docs/url-structure/#transforming-images-using-url, https://www.pixelbin.io/docs/transformations/ml/watermark-remover/

# this basically auto-runs the transformation if you submit the properly formatted URL;
# instead of literally navigating to the URL, this executes the same functionality via background HTTP GET requests
# in this case, the cloud-name is functionally like the API key that allows it to be authenticated

def auto_delete_original_image_from_storage(): 
    # STEP 3 = AUTO-DELETE ORIGINAL IMAGE FROM PIXELBIN STORAGE, POST-TRANSFORMATION + DOWNLOAD
    # DOCUMENTATION FOR API-BASED FILE DELETION: https://github.com/pixelbin-dev/pixelbin-python-sdk/blob/main/documentation/platform/ASSETS.md#deletefile
    
    print("Pixelbin image deletion from storage in progress...")
    
    try: # I don't redefine the config stuff / permissions here, because that was done earlier during fileUpload
        result = pixelbin.assets.deleteFile( # sync method call
            fileId=f"IMAGE_EDITING_FOLDER/{FilenameModifiedByPixelbin}")
        print("Image deleted from Pixelbin storage!")
        # print(result)
    except Exception as e:
        print(e)

import requests
import time

def fetch_image(url):
    print("Pixelbin image transformation in progress...")
    response = requests.get(url)

    # continues functionally "refreshing" the CDN URL until the transformation is complete and the image is ready for download
    # note that this does NOT continue to fire additional identical jobs + use up credits each time
    # that's because once the exact specific job is sent via the specific "send job" URL? it simply runs until completed
    # and you'll get a 202 code that simply says: "that job is currently in progress, please wait..."
    # by "rerunning" it after fully completed too? it'll just auto-download the already-completed image, not re-run the original job
    while response.status_code == 202: # status code 202 = transformation still in progress;
        print("Transformation still processing. Waiting for 5 seconds before trying again.")
        time.sleep(5)
        response = requests.get(url)

    if 'TransformationJobError' in response.text:
        print("There was an error with the transformation.")
        return None

    if response.status_code == 200: # status code 200 = successful image transformation, ready for download;
        filepath = os.path.join(FinalImageDownloadFolder, CurrentImageFilename)
        with open(filepath, 'wb') as f:
            f.write(response.content)
        print("Image downloaded successfully!")
        auto_delete_original_image_from_storage()
        return True

    print(f"Unexpected status code: {response.status_code}")
    return None

url = f"https://cdn.pixelbin.io/v2/my_api_key_goes_here/wm.remove(rem_text:true)/IMAGE_EDITING_FOLDER/{FilenameModifiedByPixelbin}"
fetch_image(url)

原因分析

  1. 子进程输出缓冲区阻塞:子进程中有大量print输出,但subprocess.Popen默认不捕获或处理标准输出/错误。当输出填满系统缓冲区时,子进程会被阻塞,无法继续执行或终止,进而导致父进程也无法正常退出。
  2. start_new_session=True的副作用:该参数会让子进程成为新会话的首进程,脱离父进程的控制组。父进程退出时无法正确回收子进程资源,甚至可能让子进程变成孤儿进程,间接导致父进程挂起。
  3. 子进程未正确关闭文件资源:子进程中使用open()打开文件后未显式关闭,虽然Python会自动回收,但在阻塞场景下可能引发资源泄漏,影响进程终止流程。

解决方案

1. 处理子进程输出缓冲区

调用subprocess.Popen时,重定向stdout和stderr到DEVNULL(丢弃输出)或文件,避免缓冲区满阻塞:

import subprocess

# 丢弃子进程输出(Windows/Linux通用)
subprocess.Popen(
    ["python", "pixelbin-image-transformation-v3.py", files[files_index - 1], requires_editing_folder],
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL
)

# 若需保留输出用于调试,可重定向到文件
# subprocess.Popen(
#     ["python", "pixelbin-image-transformation-v3.py", files[files_index - 1], requires_editing_folder],
#     stdout=open("child_stdout.log", "a"),
#     stderr=open("child_stderr.log", "a"),
#     text=True
# )

2. 移除不必要的start_new_session=True

如果没有特殊需求(如让子进程脱离终端控制),删除该参数,让子进程处于父进程控制组内,便于父进程退出时正确回收资源。

3. 子进程中显式管理文件资源

使用with语句自动管理文件句柄,避免资源泄漏:

try:
    print("Pixelbin image upload in progress...")
    with open(CurrentFilepathFull, "rb") as file_handle:
        result = pixelbin.assets.fileUpload(
            file=file_handle,
            path="IMAGE_EDITING_FOLDER",
            name=CurrentImageFilename,
            access="public-read",
            tags=["tag1","tag2"],
            metadata={},
            overwrite=True,
            filenameOverride=True)
    print("Image uploaded successfully!")
except Exception as e:
    print(e)

4. 父进程退出时清理子进程(可选)

若需确保父进程退出时子进程也终止,可监听窗口关闭事件,显式终止所有子进程:

# 父脚本中添加全局变量保存子进程对象
child_processes = []

def move_current_image():
    global files_index, child_processes
    # ... 原有代码 ...
    proc = subprocess.Popen(
        ["python", "pixelbin-image-transformation-v3.py", files[files_index - 1], requires_editing_folder],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )
    child_processes.append(proc)
    # ... 原有代码 ...

# 监听窗口关闭事件
def on_closing():
    for proc in child_processes:
        proc.terminate()  # 终止子进程
        proc.wait()       # 等待子进程退出
    root.destroy()

root.protocol("WM_DELETE_WINDOW", on_closing)

内容的提问来源于stack exchange,提问作者king_anton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 10:33:08