V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
gzlock
V2EX  ›  Python

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

  •  
  •   gzlock ·
    gzlock · 2020-05-21 19:06:25 +08:00 · 4372 次点击
    这是一个创建于 1681 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    • 实际的项目中用 threading.Thread()解决了,但是不甘心还是想试试用异步解决,但还没找到解决的方法

    • 就目前的体验来说,python 异步用起来的体验没有 node.js 来的舒服,限制挺多的

    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()
    
    
    
    19 条回复    2020-05-25 19:23:03 +08:00
    ClericPy
        1
    ClericPy  
       2020-05-21 19:27:28 +08:00
    你的这个... asyncio.create_task(self.exec()) 得到的是个 asyncio.Task 对象, 你到底要不要阻塞, 我怎么感觉你该做的是总协程丢在外面, window 对象丢到 run_in_executor 里呢...



    python3 的 await 如果能自动判断这个关键字后面的是否 awaitable 多好, 现在太麻烦了, 还得自己判断
    ipwx
        2
    ipwx  
       2020-05-21 19:33:22 +08:00
    aio eventloop 需要独占一个线程,GUI 也需要独占一个线程。所以你永远需要至少两个线程。
    gzlock
        3
    gzlock  
    OP
       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
        4
    renmu123  
       2020-05-21 21:56:05 +08:00 via Android
    我对异步的理解是同时只有一个线程在工作,在休息的时候可以进行调度,因为 while 循环要不停进行工作,主渲染进程自然会卡住,因为只有一个在工作
    Mahaha
        5
    Mahaha  
       2020-05-21 22:19:04 +08:00 via Android
    可以试试在 在总入口 if __name__ == "__main__":下面这样子 asyncio.gather(window.xx(), window.exec()) 手机打字大概是这样子
    muzuiget
        6
    muzuiget  
       2020-05-21 22:44:01 +08:00
    查查 tkinter 是否有 idle 之类的 callback 接口,或者非阻塞更新。
    imn1
        7
    imn1  
       2020-05-21 23:00:00 +08:00
    不管是否协程,写 GUI 本身就要避免用 while 无条件循环,想好了退出条件在哪里激活,退出条件在外部激活等于没有条件

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

    我想说的是,这个问题问的就不对,] —— [ 真想学,你看看 PY 开源 UI 库怎么写好了
    ipwx
        10
    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
        11
    wizardoz  
       2020-05-22 09:59:14 +08:00
    你这是需要线程吧
    no1xsyzy
        12
    no1xsyzy  
       2020-05-22 10:49:47 +08:00
    @ipwx #2 GUI 不一定独占线程,只不过独占方式写起来不用烧脑和脏处理。
    不过似乎 tkinter 没有处理单轮事件的方式,tk 的 mainloop 好像不在 python 里甚至没触发 GIL 锁?而且实际上 GUI 部分也在不同线程里 mainloop 更像是 join ?没仔细测试。
    ipwx
        13
    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
        14
    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
        15
    ipwx  
       2020-05-22 14:59:04 +08:00
    @no1xsyzy 我感觉吧,楼主这只是个 demo 。

    GUI 调用协程是有实际价值的。譬如你用 requests,用线程池做下载器,并发才多少。用上 aiohttp,并发一下子暴涨。你只需要用 asyncio 的 queue (那个是线程安全的好像)把 task 塞进 asyncio 里面,最后在主线程通过 event 把结果弄回来就行了。
    no1xsyzy
        16
    no1xsyzy  
       2020-05-22 15:13:20 +08:00
    @ipwx #15 我是说把 GUI 操作、事件处理什么的都塞进协程里,只留一个 loop 是不太可能的,也很吊诡。
    至于某些 worker 是单线程做协程还是线程池是另一个问题。
    ipwx
        17
    ipwx  
       2020-05-22 15:55:00 +08:00
    @no1xsyzy 对呀,我上面的基本看法也是两个线程呀。我从来不觉得 GUI 用协程搞是好主意啊。
    no1xsyzy
        18
    no1xsyzy  
       2020-05-22 15:57:36 +08:00
    @ipwx #17 电波稍有失真,你我把同一件事两种不同方法表达两遍了吧……
    就这样吧……
    ppgs8903
        19
    ppgs8903  
       2020-05-25 19:23:03 +08:00
    @ipwx 就当是吧,刚和几个国外的大仙聊完,只不过在聊 BIO NIO 其实差不多
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2840 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 08:48 · PVG 16:48 · LAX 00:48 · JFK 03:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.