Python3 里怎么让一个包含 while 循环的异步函数不断运行,而不阻塞正常的代码流程

2020-05-21 19:06:25 +08:00
 gzlock

需求是用 tkinter 制作的 gui 工具,点击 [开始] 后在异步函数里 while 循环,点击 [停止] 后让 while 停止

目前的问题是 asyncio.create_task 遇到 asyncio.sleep 就中断了

import asyncio
import time
import tkinter
from tkinter import ttk


class Window:
    def __init__(self):
        self.__do_while = False
        root = tkinter.Tk()
        root.minsize(200, 200)
        frame = ttk.Frame()
        frame.pack(fill=tkinter.BOTH)
        ttk.Button(frame, text='开始', command=self.start).pack()
        ttk.Button(frame, text='停止', command=self.stop).pack(pady=10)
        root.mainloop()

    def start(self):
        print(time.time())
        self.__do_while = True

        async def go():
            # 只 print 了一次就结束了
            asyncio.create_task(self.exec())
            
            # 界面卡住了
            # await asyncio.create_task(self.exec())
            
            # 界面卡住了
            # await self.exec()

        asyncio.run(go())
        print(time.time())

    def stop(self):
        self.__do_while = False

    async def exec(self):
        i = 0
        while self.__do_while:
            print('exec', i)
            i += 1
            await asyncio.sleep(2)


if __name__ == "__main__":
    Window()


4368 次点击
所在节点    Python
19 条回复
ClericPy
2020-05-21 19:27:28 +08:00
你的这个... asyncio.create_task(self.exec()) 得到的是个 asyncio.Task 对象, 你到底要不要阻塞, 我怎么感觉你该做的是总协程丢在外面, window 对象丢到 run_in_executor 里呢...



python3 的 await 如果能自动判断这个关键字后面的是否 awaitable 多好, 现在太麻烦了, 还得自己判断
ipwx
2020-05-21 19:33:22 +08:00
aio eventloop 需要独占一个线程,GUI 也需要独占一个线程。所以你永远需要至少两个线程。
gzlock
2020-05-21 21:14:52 +08:00
@ClericPy #1
我用 Node.js 做了个想要的功能 https://repl.it/@gzlock/ChillyLightblueHashmap
需求就是:
1,不用额外的线程进程啥的(Node.js 那个例子里也没有用到)
2,在异步函数里不断循环
3,在异步函数外部可以中断异步函数里的循环
就目前按我的尝试来说,python 做不到

@ipwx #2 那我用 threading.Thread 算是提前解决问题啦🐶
renmu123
2020-05-21 21:56:05 +08:00
我对异步的理解是同时只有一个线程在工作,在休息的时候可以进行调度,因为 while 循环要不停进行工作,主渲染进程自然会卡住,因为只有一个在工作
Mahaha
2020-05-21 22:19:04 +08:00
可以试试在 在总入口 if __name__ == "__main__":下面这样子 asyncio.gather(window.xx(), window.exec()) 手机打字大概是这样子
muzuiget
2020-05-21 22:44:01 +08:00
查查 tkinter 是否有 idle 之类的 callback 接口,或者非阻塞更新。
imn1
2020-05-21 23:00:00 +08:00
不管是否协程,写 GUI 本身就要避免用 while 无条件循环,想好了退出条件在哪里激活,退出条件在外部激活等于没有条件

其次,先不管能否实现,光看代码,stop()或者主窗口没有任何能接收 async 信号的代码,那它跟协程就没关系,只能等整个协程结束才能工作
Nich0la5
2020-05-21 23:29:41 +08:00
Python 协程不是用来做这个的啊,await 之后当前协程会挂起,不知道什么时候才重新拿起来,和游戏的即时性要求不符。
协程主要用于 io 密集型应用而非 CPU 密集型
ppgs8903
2020-05-22 09:08:02 +08:00
@ipwx 如果这都能问的话,我瞎猜下,PY 调用 C 控制 DMA 设备单独中断 CPU 就好了。aio 看底层对什么设备控制吧,BUF 如果在和 CPU 无关只是给 CPU 一个最高优先级的通知(通知 地址和长度),或者 直接 DMA 和 显示器控制芯片直接互相通信。这就有点飘了,而且不是特别特殊的场景也不这么搞。

我想说的是,这个问题问的就不对,] —— [ 真想学,你看看 PY 开源 UI 库怎么写好了
ipwx
2020-05-22 09:32:49 +08:00
@ppgs8903 你在说啥?完全牛头不对马嘴吧。。。

@Nich0la5 在 GUI 程序里面用 aio eventloop 没啥问题吧,用啥不能用。关键是楼主用错了。

- - - -

@gzlock

要理解 aio,其实最好去看一看 select/epoll 的资料。Python 的 async 语法只是一堆语法糖,本质就是让人写 eventloop 程序更容易。然后 GUI 又是一个典型的 eventloop,有兴趣可以看看 Windows API 写 GUI 的那套,或者看看 Qt 源代码。但不管是什么类型的 eventloop,都需要有一个 while loop 来 receive event -> process event 。

既然有两个 eventloop,那么自然需要两个线程去各自跑这两个 eventloop 。

asyncio.run() 就是跑那么一个 loop,内部我虽然没看过代码,但本质肯定等价于一个 while loop,直到所有协程跑完再退出。如果你在 gui 的 main thread 里面跑,就相当于阻塞了 gui 的 eventloop 。
wizardoz
2020-05-22 09:59:14 +08:00
你这是需要线程吧
no1xsyzy
2020-05-22 10:49:47 +08:00
@ipwx #2 GUI 不一定独占线程,只不过独占方式写起来不用烧脑和脏处理。
不过似乎 tkinter 没有处理单轮事件的方式,tk 的 mainloop 好像不在 python 里甚至没触发 GIL 锁?而且实际上 GUI 部分也在不同线程里 mainloop 更像是 join ?没仔细测试。
ipwx
2020-05-22 11:17:23 +08:00
@no1xsyzy

1 、独占线程和 GIL 锁没有任何关系。一个 C 语言写的线程完全可以进入 while 之前释放 GIL 锁,在调用任何 python 函数之前获取 GIL 锁。tk 的 mainloop 是 C 模块。

搜了一下: https://github.com/python/cpython/blob/master/Modules/_tkinter.c#L2861

对于等待。确实 GUI 一般不是忙等待。像 Windows API 是调用操作系统的 API 获取下一个 event,而操作系统内部必然有队列,不是忙等待。比如

Windows: https://docs.microsoft.com/en-us/windows/win32/learnwin32/window-messages
xlib: http://mech.math.msu.su/~nap/2/GWindow/xintro.html

mac 没了解过,不过大概差不多,不然很难想象 Qt 那种库该怎么写,因为从上到下都充斥着 event-loop 的味道(比如别的线程要更新界面必须发送一个消息到 GUI 主线程)。所以你见过的不是 event loop 的 GUI 库(比如 Qt )大部分情况下只是给你把操作系统的 eventloop 包装了一下而已。
no1xsyzy
2020-05-22 11:53:01 +08:00
@ipwx #13 是没有关系,几句话没排版没调序……
协程方式写 GUI,从 Data flow 层面上看是很诡异的,不同 Data flow 还可能发生竞争和歧义。
协程方式写,一个问题就是需要把 asyncio 的 event loop 和 tk 的 mainloop 合并,因为后者似乎不支持任意 task 或者 asyncio 的 Task 的缘故,理应把后者并入前者,也就是把 mainloop 的行为写成一个调用结束事会把自己加进 task 的函数,所以需要单 event 处理,然而似乎没有。
另一个问题就是协程不知道什么时候会被放出来可能导致类似 vb 那样复杂计算放在按钮事件里会导致假死,必须常常 DoEvents 主动释放。
测试时 tk.Tk() 调用完就有空窗口了,而且 REPL 还活着,并且这个窗口也是可以任意改变大小和被关闭的。这两个在 Windows 下都是需要处理 event 的。我不太清楚 tkapp.mainloop 到底干了啥……
ipwx
2020-05-22 14:59:04 +08:00
@no1xsyzy 我感觉吧,楼主这只是个 demo 。

GUI 调用协程是有实际价值的。譬如你用 requests,用线程池做下载器,并发才多少。用上 aiohttp,并发一下子暴涨。你只需要用 asyncio 的 queue (那个是线程安全的好像)把 task 塞进 asyncio 里面,最后在主线程通过 event 把结果弄回来就行了。
no1xsyzy
2020-05-22 15:13:20 +08:00
@ipwx #15 我是说把 GUI 操作、事件处理什么的都塞进协程里,只留一个 loop 是不太可能的,也很吊诡。
至于某些 worker 是单线程做协程还是线程池是另一个问题。
ipwx
2020-05-22 15:55:00 +08:00
@no1xsyzy 对呀,我上面的基本看法也是两个线程呀。我从来不觉得 GUI 用协程搞是好主意啊。
no1xsyzy
2020-05-22 15:57:36 +08:00
@ipwx #17 电波稍有失真,你我把同一件事两种不同方法表达两遍了吧……
就这样吧……
ppgs8903
2020-05-25 19:23:03 +08:00
@ipwx 就当是吧,刚和几个国外的大仙聊完,只不过在聊 BIO NIO 其实差不多

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/674125

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX