保持程序打开直到按下键 - 使用线程和子进程 - 不需要的密钥拦截

时间:2018-01-04 03:31:09

标签: python multithreading subprocess

tl; dr:我有几个线程,一个是监听input()的线程,以保持程序在keypress上运行/退出。但是在程序中有一次我需要停止这个监听器,否则它将截取子处理程序的输入。

长版:
- 程序应下载一些数据,然后将其交给其他一些控制台程序进行处理 - 程序应该在下载完成之前运行或直到发送ENTER-keypress为止 - 在这两种情况下,下载线程都将正常结束,并且应完成外部处理 - 问题:input()函数仍在侦听并拦截子进程控制台程序的第一个输入。

import os
import subprocess
import threading
import time


def thread_do_downloads():
    # does some downloads and will set the flag "flag_download_completed=True" 
    # eventually to signal download completed
    # for this example just set the flag
    global flag_download_completed
    flag_download_completed = True


def do_stuff_with_downloaded_data():
    # this is of course not the program I would call,
    # but this example should show how the input would be intercepted

    if os.name == 'nt':
        parameters = ["set", "/p", "variable=Press Enter"]        # for this example (Windows) call "set", this program will wait for a user input
    else:
        parameters = ["read", "variable"]                        # hope this works for linux...
    p1 = subprocess.Popen(parameters, shell=True)
    p1.communicate()


def listen_for_keypress():
    input()
    print("keypress intercepted")


def main():
    dl = threading.Thread(target=thread_do_downloads)
    dl.start()

    kill_listener = threading.Thread(target=listen_for_keypress, daemon=True)   # daemon: to not have it lingering after main thread is done
    kill_listener.start()


    print("Press ENTER to stop downloading.")
    while True:
        if not kill_listener.is_alive() or flag_download_completed:
            break
        time.sleep(1)

    # here are some lines to make sure the download thread above completes gracefully
    do_stuff_with_downloaded_data()
    print("All done")


if __name__ == '__main__':
    flag_download_completed = False
    main()

将导致:
按ENTER键停止下载 按Enter <<停在这里直到我按下ENTER
keypress截获<<停在这里直到我按下ENTER

全部完成

1 个答案:

答案 0 :(得分:0)

如果你可以将主线程放在控制台的顶部,也许你可以利用input()将阻止主线程直到Enter被按下为止的事实。一旦执行继续(因为按下Enter),请与正在运行的线程通信,他们必须停止使用Event(另一个示例here)。如果你确实想听S.O.信号,我建议你看看signal模块(注意,一些功能可能是O.S依赖)。

import threading
import time


def thread_do_downloads(stop_activated):
    # does some downloads and will set the flag "flag_download_completed=True"
    # eventually to signal download completed
    # for this example just set the flag
    global flag_download_completed
    while not stop_activated.is_set():
        time.sleep(0.5)
        print("ZZZZZZZ")


def do_stuff_with_downloaded_data():
    print("doing stuff with downloaded data")


def main():
    stop_activated = threading.Event()

    dl = threading.Thread(target=thread_do_downloads, args=(stop_activated,))
    dl.start()

    input("Press ENTER to stop downloading.")
    stop_activated.set()
    print("stopping (waiting for threads to finish...)")
    dl.join()

    # here are some lines to make sure the download thread above completes gracefully
    do_stuff_with_downloaded_data()
    print("All done")


if __name__ == '__main__':
    main()

编辑(根据OP的评论):

原始问题的一个复杂问题是如何将终止请求传递给子进程。因为进程不与父进程(产生进程的进程)共享内存,所以实际上只能通过实际的SO信号(或几乎只)来完成。由于这种内存隔离,父进程上设置的任何标志都不会对生成的子进程产生影响:进程间通信的唯一方法是通过操作系统信号,或通过父级和子级的文件(或类文件结构)过程“已知”并用于共享信息。此外,在父级中调用input()会将标准输入(stdin)绑定到该进程,这意味着默认情况下,子进程不知道父级中按下的键(您可以始终绑定{{ 1}}子进程到父进程的stdin,但这会使代码更复杂一些)

幸运的是,Popen的实例确实提供了向子进程发送信号的好方法:TERM信号,子进程可以捕获并且应该将其解释为“嘿你很快就会被阻止,所以清理你的东西,关闭文件等等,然后退出“和KILL信号,这些信号对子进程没有任何意义(可以“它被捕获”:它只会杀死它(在Linux中,例如KILL信号会从被杀死的进程中删除对内存的所有访问,因此任何使用内存的操作,例如寻找下一个操作都会导致错误。更多信息{ {3}})

为了证明这一点,假设我们在主程序所在的同一目录中有一个简单的stdin文件,如下所示:

script.py >

script.py

一个需要随机时间处理的脚本,可能会很长(至少足够长时间来演示)

现在,我们可以在运行#!/usr/bin/env python import sys import random import time def main(): done = False while not done: time.sleep(0.5) print("I'm busy doing things!!") done = random.randint(0, 15) == 1 if __name__ == "__main__": main() sys.exit(0) # This is pretty much unnecessary, though 文件的步骤中创建一个(或多个)子进程,定期检查其状态(使用here),如果用户请求强制输出发送如果需要,script.py发出信号,稍后再发出TERM

KILL