Python脚本在后台运行时挂起

b.p*_*ell 5 python multithreading

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

python test.py
Run Code Online (Sandbox Code Playgroud)

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

python test.py &
Run Code Online (Sandbox Code Playgroud)

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

为了允许程序从终端/后台运行,我是否需要在 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()
Run Code Online (Sandbox Code Playgroud)

met*_*ter 5

为了允许程序从终端/后台运行,我是否需要在 raw_input 之前放置一个 if 语句来检查它是否在后台或者我是否缺少其他有帮助的语句?

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

import os

while True:
    userInput = ""
    if os.getpgrp() == os.tcgetpgrp(sys.stdout.fileno()):
        userInput = raw_input("")
    time.sleep(1)
Run Code Online (Sandbox Code Playgroud)

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

另一个问题提出了一个类似的问题,我有一个更长的解释:在后台时冻结标准输入,在前台时解冻


更好的方法是将它与标准select.pollI/O 分开(通过/dev/tty直接使用)解决交互式 I/ O,因为您不希望 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()
Run Code Online (Sandbox Code Playgroud)

仍然需要后台/前台检测,因为进程没有完全从 shell 分离(因为它可以被带回前台)因此如果有任何输入发送到 shell,poll将返回filenotty 的 ,并且如果这触发了readline 然后将停止该过程。

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

$ python script.py < script.py 2> stderr
input stream length: 2116
Run Code Online (Sandbox Code Playgroud)

在下面的完整示例中,它还向用户提供了提示,即>每当发送命令或进程返回前台时都会显示 a,并将整个事物包装在一个main函数中,并修改了第二个线程以吐出事物在标准错误:

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
Run Code Online (Sandbox Code Playgroud)

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

最后一个注意事项:这不是跨平台的,它不能在 Windows 上运行,因为它没有select.poll/dev/tty