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)
原因分析
- 子进程输出缓冲区阻塞:子进程中有大量
print输出,但subprocess.Popen默认不捕获或处理标准输出/错误。当输出填满系统缓冲区时,子进程会被阻塞,无法继续执行或终止,进而导致父进程也无法正常退出。 start_new_session=True的副作用:该参数会让子进程成为新会话的首进程,脱离父进程的控制组。父进程退出时无法正确回收子进程资源,甚至可能让子进程变成孤儿进程,间接导致父进程挂起。- 子进程未正确关闭文件资源:子进程中使用
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
相关产品推荐
相关产品推荐

