使用选择器的非阻塞标准输入

djv*_*jvg 5 python select stdin

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

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

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

import sys

character = ''
while character != '\n':
    character = sys.stdin.read(1)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

这可能有一个很好的理由,但我目前没有看到,也找不到任何相关问题。

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

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

tob*_*bib 0

sys.stdin是 的实例,io.TextIOWrapper而 又包装 的实例io.BufferedReader

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

$ python foo.py 
# enter asd\n
'a'
# enter \n
's'
'd'
'\n'
Run Code Online (Sandbox Code Playgroud)

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

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

$ python foo.py 
# enter asd\n
b'a'
b's'
b'd'
b'\n'
Run Code Online (Sandbox Code Playgroud)

编辑:有关 cbreak 模式的一些上下文

man 3 cbreak

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

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

$ python foo.py 
# enter a
'a'
# enter s
's'
# enter d
'd'
# enter \n
'\n'
Run Code Online (Sandbox Code Playgroud)