【问题标题】:How to manually exit an infinite trio-loop, like the trio's tutorial echo client如何手动退出无限三重奏循环,如三重奏的教程回显客户端
【发布时间】:2019-06-17 13:06:59
【问题描述】:

除了使用Ctrl-C 或使用超时之外,有没有办法手动退出三重奏无限循环,例如三重奏教程中的 echo 客户端 https://trio.readthedocs.io/en/latest/tutorial.html#an-echo-client

我的想法是使用从另一个 python 脚本调用 echo 客户端,并且也可以使用相同的 python 脚本任意关闭它。我正在考虑使用一个标志(也许是事件?)作为开关来触发托儿所中的cancel_scope.cancel()。但我不知道如何触发开关。下面是我修改教程回显客户端代码的尝试。

import sys
import trio

PORT = 12345
BUFSIZE = 16384
FLAG = 1 # FLAG is a global variable

async def sender(client_stream):
    print("sender: started")
    while FLAG:
        data = b'async can sometimes be confusing but I believe in you!'
        print(f"sender: sending {data}")
        await client_stream.send_all(data)
        await trio.sleep(1)

async def receiver(client_stream):
    print("recevier: started!")
    while FLAG:
        data = await client_stream.receive_some(BUFSIZE)
        print(f"receiver: got data {data}")
        if not data:
            print("receiver: connection closed")
            sys.exit()

async def checkflag(nursery): # function to trigger cancel()
    global FLAG
    if not FLAG:
        nursery.cancel_scope.cancel()
    else:
        # keep this task running if not triggered, but how to trigger it, 
        # without Ctrl-C or timeout?
        await trio.sleep(1) 

async def parent():
    print(f"parent: connecting to 127.0.0.1:{PORT}")
    client_stream = await trio.open_tcp_stream("127.0.0.1", PORT)
    async with client_stream:
        async with trio.open_nursery() as nursery:
            print("parent: spawning sender ...")
            nursery.start_soon(sender, client_stream)

            print("parent: spawning receiver ...")
            nursery.start_soon(receiver, client_stream)

            print("parent: spawning checkflag...")
            nursery.start_soon(checkflag, nursery)

        print('Close nursery...')
    print("Close stream...")

trio.run(parent)

我发现我无法在trio.run() 之后将任何命令输入python REPL,手动更改FLAG,我想知道我是否从另一个脚本调用此回显客户端,如何准确触发@987654327 @在幼儿园?或者,还有更好的方法?非常感谢所有帮助。谢谢。

【问题讨论】:

    标签: python python-trio


    【解决方案1】:

    如果您想使用键盘输入退出,这里有适用于 Linux 和 Mac OS X 的解决方案。您可以使用 Python msvcrt 模块在 Windows 上执行类似操作。

    我从the Trio tutorial 复制了echo-client.py,并在添加的三个代码块上添加了“新”注释。在 REPL 中,您可以键入 'q' 以取消 Nursery 范围并退出:

    # -- NEW
    import termios, tty
    
    import sys
    import trio
    
    PORT = 12345
    BUFSIZE = 16384
    
    # -- NEW
    async def keyboard():
        """Return an iterator of characters from stdin."""
        stashed_term = termios.tcgetattr(sys.stdin)
        try:
            tty.setcbreak(sys.stdin, termios.TCSANOW)
            while True:
                yield await trio.run_sync_in_worker_thread(
                    sys.stdin.read, 1,
                    cancellable=True
                )
        finally:
            termios.tcsetattr(sys.stdin, termios.TCSANOW, stashed_term)
    
    async def sender(client_stream):
        print("sender: started!")
        while True:
            data = b"async can sometimes be confusing, but I believe in you!"
            print("sender: sending {!r}".format(data))
            await client_stream.send_all(data)
            await trio.sleep(1)
    
    async def receiver(client_stream):
        print("receiver: started!")
        while True:
            data = await client_stream.receive_some(BUFSIZE)
            print("receiver: got data {!r}".format(data))
            if not data:
                print("receiver: connection closed")
                sys.exit()
    
    async def parent():
        print("parent: connecting to 127.0.0.1:{}".format(PORT))
        client_stream = await trio.open_tcp_stream("127.0.0.1", PORT)
        async with client_stream:
            async with trio.open_nursery() as nursery:
                print("parent: spawning sender...")
                nursery.start_soon(sender, client_stream)
    
                print("parent: spawning receiver...")
                nursery.start_soon(receiver, client_stream)
    
                # -- NEW
                async for key in keyboard():
                    if key == 'q':
                        nursery.cancel_scope.cancel()
    
    trio.run(parent)
    

    tty.setcbreak 的调用将终端置于无缓冲模式,因此您不必在程序接收输入之前按回车键。它还可以防止字符被回显到屏幕上。此外,顾名思义,它允许Ctrl-C 正常工作。

    finally 块中,termios.tcsetattr 将终端恢复到tty.setcbreak 之前的任何模式。所以你的终端在退出时恢复正常。

    sys.stdin.read 在单独的线程中生成,因为它需要在阻塞模式下运行(在异步上下文中不好)。原因是stdinshares its file descriptionstdoutstderr。将stdin 设置为非阻塞也会将stdout 设置为非阻塞作为副作用,这可能会导致print 函数出现问题(在我的例子中是截断)。

    进程间通信

    下面是一个使用套接字从另一个进程中取消一个 Trio 进程的基本示例:

    # infinite_loop.py    
    import trio    
    
    async def task():     
        while True:       
            print("ping")    
            await trio.sleep(0.5)    
    
    async def quitter(cancel_scope):      
        async def quit(server_stream):    
            await server_stream.receive_some(1024)    
            cancel_scope.cancel()    
        await trio.serve_tcp(quit, 12346)    
    
    async def main():    
        async with trio.open_nursery() as nursery:    
            nursery.start_soon(task)    
            nursery.start_soon(quitter, nursery.cancel_scope)    
    
    trio.run(main)
    
    # slayer.py        
    import trio    
    
    async def main():    
        async with await trio.open_tcp_stream("127.0.0.1", 12346) as s:
            await trio.sleep(3)    
            await s.send_all(b'quit')    
    
    trio.run(main)
    

    【讨论】:

    • 这里要注意的关键点:一旦你在某个决定终止服务器的 Trio 任务中运行了一些代码,它就可以执行nursery.cancel_scope.cancel()。所有棘手的事情都在于确定是否按下了某个键。最终 Trio 将有一个更简单的方法来做到这一点,但控制台输入非常复杂,所以在实现之前可能需要一点时间:github.com/python-trio/trio/issues/174
    • 感谢您的代码,它完美无缺。但是如果我想从另一个脚本(从另一个进程)触发取消,我是否还需要在这里生成一个单独的线程?
    • 如果您有两个并行运行的进程,您将需要使用更通用的进程间通信 (IPC) 技术。 Trio 偶数循环在单个线程中运行,它最接近 IPC 的是trio.BlockingTrioPortal,它允许直接的线程间通信。在这种情况下,我将在nursery 中生成另一个任务,该任务在socket 上侦听q“退出”消息(类似于上面的keyboard)。有时间我会编辑我的答案。
    • 非常感谢!这正是我需要的!
    【解决方案2】:

    有很多方法可以做到这一点。为什么不直接使用Ctrl-C?对我来说似乎完全有效。

    如果你真的不想使用Ctrl-C,那么你将需要一个函数来监听输入并更新FLAG(或者直接退出程序;我认为你不需要@老实说,987654324@ 逻辑在这里)。

    例如,您可以拥有一个从文件轮询/从数据库读取/侦听终端输入等的函数,并使其并行运行。侦听器应在同一 Python 脚本中作为单独的工作人员运行。但取决于您选择如何执行此操作,更改外部输入的函数可以是一个独立的脚本

    【讨论】:

    • 感谢您的回复。想问一下,如何写一个函数来监听终端输入?是通过子流程模块吗?
    • 您不需要使用子流程。尝试使用input() 命令。不过,我并不完全确定这将如何在异步设置中工作。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-16
    • 2012-03-17
    • 1970-01-01
    • 1970-01-01
    • 2021-11-29
    • 1970-01-01
    相关资源
    最近更新 更多