【问题标题】:Python script hanging when running in the backgroundPython脚本在后台运行时挂起
【发布时间】:2015-10-02 01:36:51
【问题描述】:

我有一个 Python 脚本(在 2.7 上运行),当我从命令行和后台运行它时,它的行为会有所不同。当我从终端运行它时,它按预期运行,两个线程作为守护进程运行,将输出写入窗口,而主循环等待退出命令。它永远运行,直到我输入退出:

python test.py

当同一个程序在后台运行时,两个线程都运行一次,然后程序挂起(我已将其范围缩小到 raw_input,我想我做了一个错误的假设,即两个线程会继续运行,即使在后台运行,而 raw_input 阻塞了主线程。例如,由于在这种情况下没有输入,因此两个线程基本上将永远运行)。

python test.py &

我的目标是让一个程序运行这些循环(可能永远),但如果我从终端运行它会接受输入。

为了让程序在终端/后台运行,我是否需要在 raw_input 之前放置一个 if 语句来检查它是否在后台,或者我是否遗漏了其他有用的语句?

import sys
import time
from threading import Thread

def threadOne():
    while True:
        print("Thread 1")
        time.sleep(1)

def threadTwo():
    while True:
        print("Thread 2")
        time.sleep(1)

# Run the threads in the background as daemons
threadOne = Thread(target = threadOne)
threadOne.daemon = True
threadOne.start()

threadTwo = Thread(target = threadTwo)
threadTwo.daemon = True
threadTwo.start()

# Main input loop.  This will allow us to enter input.  The
# threads will run forever unless "quit" is entered.  This
# doesn't run when the program is run in the background (I
# incorrectly assumed it would just run forever with no input 
# ever being entered in that scenario).
while True:
    userInput = ""
    userInput = raw_input("")
    time.sleep(1)

    # This should allow us to exit out
    if str(userInput) == "quit":
        sys.exit()

【问题讨论】:

    标签: python multithreading


    【解决方案1】:

    为了让程序在终端/后台运行,我是否需要在 raw_input 之前放置一个 if 语句来检查它是否在后台,或者我是否遗漏了其他有用的语句?

    在某种程度上,这可能有效(我假设您在 *nix 上运行它),但是如果用户要将进程发送回后台(即使用 Ctrl 暂停它Z 然后在后台使用%&) 恢复它,而raw_input 正在等待用户输入,然后stdin 上的读取将被阻止,因为它在后台,从而导致内核停止进程,因为这就是 stdio 的工作方式。如果这是可以接受的(基本上用户必须在暂停进程之前按回车键),您可以简单地这样做:

    import os
    
    while True:
        userInput = ""
        if os.getpgrp() == os.tcgetpgrp(sys.stdout.fileno()):
            userInput = raw_input("")
        time.sleep(1)
    

    os.getpgrp 所做的是返回当前 os 组的 id,然后os.tcgetpgrp 获取与该进程的 stdout 关联的进程组,如果它们匹配,则表示该进程当前在前台,这意味着您可能可以在不阻塞线程的情况下调用raw_input

    另一个问题提出了类似的问题,我有一个更长的解释:Freeze stdin when in the background, unfreeze it when in the foreground


    更好的方法是将它与 select.poll 结合起来,并从标准 I/O 中单独处理交互式 I/O(直接使用 /dev/tty),因为您不希望 stdin/stdout 重定向被“污染”那。这是包含这两个想法的更完整版本:

    tty_in = open('/dev/tty', 'r')
    tty_out = open('/dev/tty', 'w')
    fn = tty_in.fileno()
    poll = select.poll()
    poll.register(fn, select.POLLIN)
    
    while True:
        if os.getpgrp() == os.tcgetpgrp(fn) and poll.poll(10):  # 10 ms
            # poll should only return if the input buffer is filled,
            # which is triggered when a user enters a complete line,
            # which lets the following readline call to not block on
            # a lack of input.
            userInput = tty_in.readline()
            # This should allow us to exit out
            if userInput.strip() == "quit":
                sys.exit()
    

    仍然需要后台/前台检测,因为进程没有完全脱离外壳(因为它可以被带回前台)因此poll 将返回 tty 的fileno 如果发送任何输入进入shell,如果这触发了readline,它将停止进程。

    这种解决方案的优点是不需要用户在raw_input捕获并阻止stdin停止进程之前快速暂停任务以将其发送回后台(因为poll检查是否有要读取的输入),并允许正确的标准输入/标准输出重定向(因为所有交互式输入都通过/dev/tty 处理),因此用户可以执行以下操作:

    $ python script.py < script.py 2> stderr
    input stream length: 2116
    

    在下面的完整示例中,它还向用户提供提示,即每当发送命令或每当进程返回前台时都会显示&gt;,并将整个内容包装在main 函数中,并修改了第二个线程以在 stderr 上吐出东西:

    import os
    import select
    import sys
    import time
    from threading import Thread
    
    def threadOne():
        while True:
            print("Thread 1")
            time.sleep(1)
    
    def threadTwo():
        while True:
            # python 2 print does not support file argument like python 3,
            # so writing to sys.stderr directly to simulate error message.
            sys.stderr.write("Thread 2\n")
            time.sleep(1)
    
    # Run the threads in the background
    threadOne = Thread(target = threadOne)
    threadOne.daemon = True
    
    threadTwo = Thread(target = threadTwo)
    threadTwo.daemon = True
    
    def main():
        threadOne.start()
        threadTwo.start()
    
        tty_in = open('/dev/tty', 'r')
        tty_out = open('/dev/tty', 'w')
        fn = tty_in.fileno()
        poll = select.poll()
        poll.register(fn, select.POLLIN)
    
        userInput = ""
        chars = []
        prompt = True
    
        while True:
            if os.getpgrp() == os.tcgetpgrp(fn) and poll.poll(10):  # 10 ms
                # poll should only return if the input buffer is filled,
                # which is triggered when a user enters a complete line,
                # which lets the following readline call to not block on
                # a lack of input.
                userInput = tty_in.readline()
                # This should allow us to exit out
                if userInput.strip() == "quit":
                    sys.exit()
                # alternatively an empty string from Ctrl-D could be the
                # other exit method.
                else:
                    tty_out.write("user input: %s\n" % userInput)
                    prompt = True
            elif not os.getpgrp() == os.tcgetpgrp(fn):
                time.sleep(0.1)
                if os.getpgrp() == os.tcgetpgrp(fn):
                    # back to foreground, print a prompt:
                    prompt = True
    
            if prompt:
                tty_out.write('> ')
                tty_out.flush()
                prompt = False
    
    if __name__ == '__main__':
        try:
            # Uncomment if you are expecting stdin
            # print('input stream length: %d ' % len(sys.stdin.read()))
            main()
        except KeyboardInterrupt:
            print("Forcibly interrupted.  Quitting")
            sys.exit()  # maybe with an error code
    

    这是一个有趣的练习;如果我可以说,这是一个相当好的和有趣的问题。

    最后一点:这不是跨平台的,它不会在 Windows 上运行,因为它没有 select.poll/dev/tty

    【讨论】:

    • 感谢您的回答和清晰的解释,非常感谢。这可以解决问题,在我的 Ubuntu 和 Raspberry Pi 安装上对其进行了测试,它现在的行为似乎就像我正在拍摄的那样。
    • @b.pell 我还要感谢您以简洁但又足够详细的方式表达了这个问题,以便向我们所有人展示手头的确切问题。我还更新了解决方案,以便更允许将其发送回后台供用户使用。
    猜你喜欢
    • 2015-08-19
    • 2013-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-09
    相关资源
    最近更新 更多