使用选择器的非阻塞 STDIN

non-blocking stdin using selectors

提问人:djvg 提问时间:1/2/2021 最后编辑:djvg 更新时间:1/8/2023 访问量:802

问:

玩弄非阻塞控制台输入,将 Python 的选择器与 结合使用,有一些我不明白的地方:sys.stdin

假设我想在用户按下时退出循环,可能是在先输入其他一些字符之后。Enter

如果我执行阻塞读取,如下所示,则该过程始终在遇到第一个换行符后完成,正如预期的那样,无论前面的任何字符如何:\n

import sys

character = ''
while character != '\n':
    character = sys.stdin.read(1)

现在考虑以下非阻塞读取的最小化示例:

import sys
import selectors

selector = selectors.DefaultSelector()
selector.register(fileobj=sys.stdin, events=selectors.EVENT_READ)

character = ''
while character != '\n':
    for key, __ in selector.select(timeout=0):
        character = key.fileobj.read(1)

如果我点击作为第一个输入,则会生成一个换行符,并且该过程将按预期完成。Enter

但是,如果我先输入一些其他字符,然后输入 ,则该过程不会完成:我需要再次点击才能完成。EnterEnter

显然,这种实现仅在换行符是第一个输入时才有效。

这可能是有充分理由的,但我目前没有看到它,也找不到任何相关问题。

这是否与我的非阻塞实现有关,或者是缓冲区问题,或者可能与控制台或终端实现有关?stdin

(我正在从 ubuntu 上的 python 3.8 shell 运行它。

python 选择 stdin

评论

1赞 AKX 1/3/2021
请参阅 stackoverflow.com/a/43929760/51685 - 我认为终端模式在这里是相关的......
0赞 djvg 1/3/2021
@AKX:谢谢!将终端设置为模式似乎确实有效。cbreak
0赞 djvg 1/4/2021
@AKX:我仍然想知道为什么这只发生在实现中。selectors
0赞 AKX 1/4/2021
使用底层二进制文件会有所作为吗?sys.stdin.buffer
0赞 djvg 1/9/2023
请注意,第二个(以及之前的任何输入)在 python 进程退出后由 shell 处理。<enter>

答:

0赞 tobib 1/7/2023 #1

sys.stdin是 IO 的一个实例。TextIOWrapper,它反过来包装 io 的实例。BufferedReader

添加到代码的末尾,让您看看发生了什么:print(repr(character))

$ python foo.py 
# enter asd\n
'a'
# enter \n
's'
'd'
'\n'

当您第一次输入“asd\n”时,python 会将其全部读取到缓冲区中,但只返回第一个字符。由于基础 stdin 现在是空的,因此下一次调用将再次阻塞。当您输入另一个换行符时,选择“取消阻止”,代码将继续从缓冲区读取字符,直到到达第一个“\n”。selector.select()

可以使用 绕过缓冲。这给出了预期的行为(请注意,它返回字节而不是字符串):os.read(key.fileobj.fileno(), 1)

$ python foo.py 
# enter asd\n
b'a'
b's'
b'd'
b'\n'

编辑:关于cbreak模式的一些背景

从:man 3 cbreak

通常,tty 驱动程序会缓冲键入的字符,直到键入换行符或回车符。cbreak 例程禁用行缓冲和擦除/终止字符处理(中断和流控制字符不受影响),使用户键入的字符立即可供程序使用。nocbreak 例程将终端返回到正常(熟)模式。

在 cbreak 模式下,字符一次发送一个,因此 python 没有机会缓冲输入:

$ python foo.py 
# enter a
'a'
# enter s
's'
# enter d
'd'
# enter \n
'\n'

评论

0赞 djvg 1/9/2023
请注意,这不会阻止,因为我们显式设置了 .我们只是轮询“可供读取”事件。也可以简化为 .不幸的是,这并没有回答实际的问题,即为什么原始示例不能按预期工作。selector.select()timeout=0os.read(key.fileobj.fileno(), 1)os.read(key.fd, 1)
0赞 tobib 1/10/2023
你是对的,我错过了。不过,解释仍然成立:在初次读取后,stdin 是空的。无论是选择块还是返回空列表,效果基本相同。timeout=0
0赞 djvg 1/10/2023
对不起,但是,恕我直言,答案描述了(扩展了@AKX的评论),但没有解释。例如:“stdin 是空的”到底是什么意思?您指的是 python File 对象还是底层操作系统输入流?如果“stdin 是空的”,正如你所说,为什么在第二个之后的调用会返回剩余的字符(它们来自哪里)?为什么在读取第一个字符后没有立即检测到剩余字符?为什么突然检测到第二个之后的所有剩余字符,而不仅仅是一个字符?read(1)<enter>selectselect<enter>