大多数对这个问题的回答都建议为每个进程启动一个线程,只是为了等待那个回调。我觉得这样做是不必要的浪费:一个线程应该足够处理通过这种方式创建的所有进程的回调。
另一个回答建议使用信号,但这会导致一个竞态条件,即在前一次调用完成之前,信号处理程序可能会再次被调用。在Linux上,signalfd(2)可以解决这个问题,但它在Python中不受支持(虽然可以通过ctypes轻松添加)。
Python中asyncio使用的另一种解决方案是使用signal.set_wakeup_fd。然而,还有另一种基于操作系统将在进程退出时关闭所有打开的文件描述符的事实的解决方案。
import os
import select
import subprocess
import threading
import weakref
def _close_and_join(fd, thread):
os.close(fd)
thread.join()
def _run_poll_callbacks(quitfd, poll, callbacks):
poll.register(quitfd, select.POLLHUP)
while True:
for fd, event in poll.poll(1000.0):
poll.unregister(fd)
if fd == quitfd:
return
callback = callbacks.pop(fd)
if callback is not None:
callback()
class PollProcs:
def __init__(self):
self.poll = select.poll()
self.callbacks = {}
self.closed = False
r, w = os.pipe()
self.thread = threading.Thread(
target=_run_poll_callbacks, args=(r, self.poll, self.callbacks)
)
self.thread.start()
self.finalizer = weakref.finalize(self, _close_and_join, w, self.thread)
def run(self, cmd, callback=None):
if self.closed:
return
r, w = os.pipe()
self.callbacks[r] = callback
self.poll.register(r, select.POLLHUP)
popen = subprocess.Popen(cmd, pass_fds=(w,))
os.close(w)
print("running", " ".join(cmd), "as", popen.pid)
return popen
def main():
procs = PollProcs()
for i in range(3, 0, -1):
procs.run(["sleep", str(i)], callback=lambda i=i: print(f"sleep {i} done?"))
import time
print("Waiting...")
time.sleep(3)
if __name__ == "__main__":
main()
如果不需要支持MacOS,那么选择
select.epoll
可能是一个更好的选择,因为它允许更新正在进行的轮询。