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协程是如何实现主动调度的?
asyncio 不熟,但是应该和 eventlet 原理一致
在 eventlet 里
有一个队列一直在不停的排序,排序的 key 是时间戳
主循环一直扫这个队列, 当前时间戳>=排序 key 就调用这个 key 对应的协程
所有的协程都是在这个队列里排序等待执行…协程的 sleep 就是修改排序的时间戳让自己的调度顺序押后
lock 也是类似原理
你对应到 asyncio 里看看调度是不是这个理
在Python的asyncio里,协程的主动调度核心是靠事件循环(Event Loop)和await表达式。简单说,当一个协程执行到await某个可等待对象(比如另一个协程、Task或Future)时,它就会主动挂起自己,把控制权交还给事件循环。事件循环接着去执行其他就绪的任务,等原来那个await的东西有结果了,事件循环再在合适的时机把挂起的协程唤醒继续跑。
关键点就这几个:
await是调度点:代码里一遇到await,当前协程就暂停,事件循环接管。- 事件循环是调度器:它维护着待执行的任务队列(比如
asyncio.create_task()创建的Task)。当一个任务在await时,事件循环就从队列里找其他可运行的任务来执行。 - Task封装协程:直接用
await coro()是顺序执行。用asyncio.create_task(coro())会把协程包装成Task,这个Task会被事件循环管理,从而实现并发。多个Task在遇到await时就能互相切换。 - 底层靠
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。当task1在sleep(1)时挂起,事件循环就转去执行task2的sleep(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/
文章很长。

