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

2018-12-14 14:27:48 +08:00
 wcsjtu

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

3553 次点击
所在节点    Python
11 条回复
lolizeppelin
2018-12-14 14:57:05 +08:00
asyncio 不熟,但是应该和 eventlet 原理一致

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

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

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

lock 也是类似原理


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

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

release 可以是协程间互相切换也可以是切换到主线程那个协程
wcsjtu
2018-12-14 15:09:37 +08:00
@lolizeppelin sleep 其实还是利用的 epoll_wait 的超时, 当有 IO 事件或者超时是,epoll_wait 会被唤醒。 所以,这里的 Lock 和 sleep 还不太一样, 因为根本不知道要 lock 多久。。。。。
shylockhg
2018-12-14 15:10:16 +08:00
python 还能玩啥花样,估计就是轮询的
wcsjtu
2018-12-14 15:14:20 +08:00
@shylockhg 轮询的话,时间粒度不太好把握吧。。。。太小了浪费 CPU,太大了会导致 task 延时。。真的是这样么??
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
2018-12-14 15:28:09 +08:00
@lolizeppelin 嗯。 你说的 eventlet.sleep(0)会导致一次协程调度, 从而让其他 ready 的协程有执行的机会。 那么在 asyncio 中 Lock 的情况,release 操作应该也会触发协程调度吗?
lolizeppelin
2018-12-14 15:34:47 +08:00
不一定会切换到主循环的协程

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

看你怎么用的 原理就那样

你可以简单理解为未结束的协程之间 goto 来 goto 去
wcsjtu
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
2018-12-14 16:31:58 +08:00
asyncio 的不知道

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

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

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

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

这有一篇文章写得挺全的

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

文章很长。

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

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

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

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

© 2021 V2EX