Python中asyncio协程是如何实现主动调度的?

之前研究过 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 事件发生啊


Python中asyncio协程是如何实现主动调度的?

12 回复

asyncio 不熟,但是应该和 eventlet 原理一致

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

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

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

lock 也是类似原理


你对应到 asyncio 里看看调度是不是这个理


在Python的asyncio里,协程的主动调度核心是靠事件循环(Event Loop)和await表达式。简单说,当一个协程执行到await某个可等待对象(比如另一个协程、Task或Future)时,它就会主动挂起自己,把控制权交还给事件循环。事件循环接着去执行其他就绪的任务,等原来那个await的东西有结果了,事件循环再在合适的时机把挂起的协程唤醒继续跑。

关键点就这几个:

  1. await是调度点:代码里一遇到await,当前协程就暂停,事件循环接管。
  2. 事件循环是调度器:它维护着待执行的任务队列(比如asyncio.create_task()创建的Task)。当一个任务在await时,事件循环就从队列里找其他可运行的任务来执行。
  3. Task封装协程:直接用await coro()是顺序执行。用asyncio.create_task(coro())会把协程包装成Task,这个Task会被事件循环管理,从而实现并发。多个Task在遇到await时就能互相切换。
  4. 底层靠yield和控制流反转:协程函数本质是生成器,await有点像yield from。挂起时保存现场(局部变量、程序计数器),恢复时再加载回来。

看个简单例子就明白了:

import asyncio

async def say_after(delay, what):
    await asyncio.sleep(delay)  # 遇到await,挂起当前协程,让事件循环去干别的
    print(what)

async def main():
    # 创建两个Task,它们会被事件循环并发调度
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))

    await task1  # 等待task1完成,但此时事件循环可以调度task2
    await task2  # 等待task2完成

asyncio.run(main())

这里asyncio.sleep()模拟I/O等待。执行时,两个say_after协程都被包装成Task。当task1sleep(1)时挂起,事件循环就转去执行task2sleep(2)。虽然看起来是同时“等”,但只有一个线程,靠的就是在await点主动让出控制权。

所以,asyncio的主动调度就是协程在await时主动让出,由事件循环决定接下来跑谁。想实现并发,记得把协程包装成Task。

一句话建议:用create_task把协程扔给事件循环,然后在await点自然就会切换。

补充下

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

release 可以是协程间互相切换也可以是切换到主线程那个协程

sleep 其实还是利用的 epoll_wait 的超时, 当有 IO 事件或者超时是,epoll_wait 会被唤醒。 所以,这里的 Lock 和 sleep 还不太一样, 因为根本不知道要 lock 多久。。。。。

python 还能玩啥花样,估计就是轮询的

轮询的话,时间粒度不太好把握吧。。。。太小了浪费 CPU,太大了会导致 task 延时。。真的是这样么??



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

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

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


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

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

tornado 以前是怎么做的我不清楚, 至少 gevent evelet 应该是这样的 asyncio 应该也是差不多的
因为要解决的问题是一致的

嗯。 你说的 eventlet.sleep(0)会导致一次协程调度, 从而让其他 ready 的协程有执行的机会。 那么在 asyncio 中 Lock 的情况,release 操作应该也会触发协程调度吗?

不一定会切换到主循环的协程

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

看你怎么用的 原理就那样

你可以简单理解为未结束的协程之间 goto 来 goto 去

刚刚跟踪了 release 的执行堆栈, 有个发现: 调用 release 时, 会在 event_loop 对象的_ready 属性中,添加一个 handler, 这个 handler 估计就是唤醒 wait 这个 lock 的协程的。然后后面的就和你之前说的一样了

py<br># <a target="_blank" href="http://base_events.py" rel="nofollow noopener">base_events.py</a> lineno 1367<br>if self._ready or self._stopping:<br> timeout = 0<br>...<br># <a target="_blank" href="http://base_events.py" rel="nofollow noopener">base_events.py</a> lineno 1395<br>event_list = self._selector.select(timeout) # 立即触发调度<br>self._process_events(event_list) # 将 IO 事件的 handler 添加到_ready 中<br>...<br><br># <a target="_blank" href="http://base_events.py" rel="nofollow noopener">base_events.py</a> lineno 1431<br>handle._run() # 这个 handler 估计就是用来唤醒协程的<br>

也就是说, 当 lock 被 release 的时候, 会立即触发一次调度。 而且唤醒 wait lock 协程的 handler 一定是在 IO 事件的 handler 之前执行。。。。

asyncio 的不知道

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

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

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

asyncio 常规采用那种看他代码怎么写的就是

也正在研究 asyncio

这有一篇文章写得挺全的

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

文章很长。

回到顶部