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
wcsjtu
V2EX  ›  Python

Python3.6 asyncio 的协程是如何实现主动调度的?

  •  
  •   wcsjtu · 2018-12-14 14:27:48 +08:00 · 3728 次点击
    这是一个创建于 2173 天前的主题,其中的信息可能已经有所发展或是发生改变。

    之前研究过 tornado 的 py2.7 版本, 对 asyncio 的协程不是太熟。 据我所知,tornado 的协程调度都是依赖于 epoll_wait 的,只有 IO 事件发生才会发生协程调度,也就是说没法主动调度。 但是 asyncio 好像不是, 参见协程同步原语。比如说

    loop = asyncio.get_event_loop()
    loop.set_debug(True)
    lock = asyncio.Lock()
    async def task():
        await lock
        print("get lock now, then sleep 2s")
        await asyncio.sleep(2)
        print("wakeup")
        lock.release()
        print("sleep 1s")
        await asyncio.sleep(1)
    
    loop.run_until_complete(
        asyncio.gather(task(),  task()) 
    )
    
    input()
    

    在 asyncio 中,用 Lock 来同步的话, 协程调度机制是如何知道 Lock 已经 release 了, 然后调度正在 wait 这个锁的协程去执行的?毕竟 Lock 的 release 操作没有 IO 事件发生啊

    11 条回复    2018-12-14 18:40:54 +08:00
    lolizeppelin
        1
    lolizeppelin  
       2018-12-14 14:57:05 +08:00
    asyncio 不熟,但是应该和 eventlet 原理一致

    在 eventlet 里
    有一个队列一直在不停的排序,排序的 key 是时间戳

    主循环一直扫这个队列, 当前时间戳>=排序 key 就调用这个 key 对应的协程

    所有的协程都是在这个队列里排序等待执行...协程的 sleep 就是修改排序的时间戳让自己的调度顺序押后

    lock 也是类似原理


    你对应到 asyncio 里看看调度是不是这个理
    lolizeppelin
        2
    lolizeppelin  
       2018-12-14 15:08:59 +08:00
    补充下

    所有的 io 都是 都是其他协程切换到主线程的哪个协程
    sleep 也是其他协程切换到主线程的那个协程
    主线程的那个协程主要负责调度

    release 可以是协程间互相切换也可以是切换到主线程那个协程
    wcsjtu
        3
    wcsjtu  
    OP
       2018-12-14 15:09:37 +08:00
    @lolizeppelin sleep 其实还是利用的 epoll_wait 的超时, 当有 IO 事件或者超时是,epoll_wait 会被唤醒。 所以,这里的 Lock 和 sleep 还不太一样, 因为根本不知道要 lock 多久。。。。。
    shylockhg
        4
    shylockhg  
       2018-12-14 15:10:16 +08:00
    python 还能玩啥花样,估计就是轮询的
    wcsjtu
        5
    wcsjtu  
    OP
       2018-12-14 15:14:20 +08:00
    @shylockhg 轮询的话,时间粒度不太好把握吧。。。。太小了浪费 CPU,太大了会导致 task 延时。。真的是这样么??
    lolizeppelin
        6
    lolizeppelin  
       2018-12-14 15:20:50 +08:00
    @wcsjtu

    不是... eventlet 里的 sleep 和一般的 sleep 都一个作用
    用于放弃资源占用 和 epoll 无关

    epoll 的作用是在 io 的时候自动切换到主循环那个协程,猴子补丁也就是让你不用自己写 epoll 代码而已

    举个例子
    os.listdir 如果扫描一个大文件夹,当前协程会一直占用资源 不会切换到主线程. 其他协程就不会被调度到,会被饿死
    所以用 os.walk 来扫文件夹并加入计数器. 计数器超过一个值就调用 eventlet.sleep(0)切换到主线程


    所以你不要光盯着 io, io 耗时,大量计算也会耗时的.自然需要有放弃占用的方法的

    同样设计在 lock 里 lock 了就切换到主循环 release 就找有 lock 需要的协程切换过去

    tornado 以前是怎么做的我不清楚, 至少 gevent evelet 应该是这样的 asyncio 应该也是差不多的
    因为要解决的问题是一致的
    wcsjtu
        7
    wcsjtu  
    OP
       2018-12-14 15:28:09 +08:00
    @lolizeppelin 嗯。 你说的 eventlet.sleep(0)会导致一次协程调度, 从而让其他 ready 的协程有执行的机会。 那么在 asyncio 中 Lock 的情况,release 操作应该也会触发协程调度吗?
    lolizeppelin
        8
    lolizeppelin  
       2018-12-14 15:34:47 +08:00
    不一定会切换到主循环的协程

    有可能是 release 的时候直接切换到 lock 的协程

    看你怎么用的 原理就那样

    你可以简单理解为未结束的协程之间 goto 来 goto 去
    wcsjtu
        9
    wcsjtu  
    OP
       2018-12-14 16:06:49 +08:00
    @lolizeppelin 刚刚跟踪了 release 的执行堆栈, 有个发现: 调用 release 时, 会在 event_loop 对象的_ready 属性中,添加一个 handler, 这个 handler 估计就是唤醒 wait 这个 lock 的协程的。然后后面的就和你之前说的一样了

    ```py
    # base_events.py lineno 1367
    if self._ready or self._stopping:
    timeout = 0
    ...
    # base_events.py lineno 1395
    event_list = self._selector.select(timeout) # 立即触发调度
    self._process_events(event_list) # 将 IO 事件的 handler 添加到_ready 中
    ...

    # base_events.py lineno 1431
    handle._run() # 这个 handler 估计就是用来唤醒协程的
    ```

    也就是说, 当 lock 被 release 的时候, 会立即触发一次调度。 而且唤醒 wait lock 协程的 handler 一定是在 IO 事件的 handler 之前执行。。。。
    lolizeppelin
        10
    lolizeppelin  
       2018-12-14 16:31:58 +08:00
    asyncio 的不知道

    具体怎么做看自己需求,常见的两种

    1. release 的时候创建一个新的协程, 这个协程的内容是 switch 到 lock 的协程
    这样当前协程会继续执行剩下代码.lock 的协程排序 key 是当前的时间点, 调度排位会在前面因为 io 切换的协程之后

    2 release 的时候创建一个新的协程, 这个协程的内容是 switch 到当前协程, 然后立即切换到 lock 的协程
    这样 lock 的协程会直接被激活, 当前协程剩余代码被调度到以后再继续执行

    asyncio 常规采用那种看他代码怎么写的就是
    ucun
        11
    ucun  
       2018-12-14 18:40:54 +08:00
    也正在研究 asyncio

    这有一篇文章写得挺全的

    https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/

    文章很长。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1756 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:43 · PVG 00:43 · LAX 08:43 · JFK 11:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.