之前研究过 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 事件发生啊
1
lolizeppelin 2018-12-14 14:57:05 +08:00
asyncio 不熟,但是应该和 eventlet 原理一致
在 eventlet 里 有一个队列一直在不停的排序,排序的 key 是时间戳 主循环一直扫这个队列, 当前时间戳>=排序 key 就调用这个 key 对应的协程 所有的协程都是在这个队列里排序等待执行...协程的 sleep 就是修改排序的时间戳让自己的调度顺序押后 lock 也是类似原理 你对应到 asyncio 里看看调度是不是这个理 |
2
lolizeppelin 2018-12-14 15:08:59 +08:00
补充下
所有的 io 都是 都是其他协程切换到主线程的哪个协程 sleep 也是其他协程切换到主线程的那个协程 主线程的那个协程主要负责调度 release 可以是协程间互相切换也可以是切换到主线程那个协程 |
3
wcsjtu OP @lolizeppelin sleep 其实还是利用的 epoll_wait 的超时, 当有 IO 事件或者超时是,epoll_wait 会被唤醒。 所以,这里的 Lock 和 sleep 还不太一样, 因为根本不知道要 lock 多久。。。。。
|
4
shylockhg 2018-12-14 15:10:16 +08:00
python 还能玩啥花样,估计就是轮询的
|
5
wcsjtu OP @shylockhg 轮询的话,时间粒度不太好把握吧。。。。太小了浪费 CPU,太大了会导致 task 延时。。真的是这样么??
|
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 应该也是差不多的 因为要解决的问题是一致的 |
7
wcsjtu OP @lolizeppelin 嗯。 你说的 eventlet.sleep(0)会导致一次协程调度, 从而让其他 ready 的协程有执行的机会。 那么在 asyncio 中 Lock 的情况,release 操作应该也会触发协程调度吗?
|
8
lolizeppelin 2018-12-14 15:34:47 +08:00
不一定会切换到主循环的协程
有可能是 release 的时候直接切换到 lock 的协程 看你怎么用的 原理就那样 你可以简单理解为未结束的协程之间 goto 来 goto 去 |
9
wcsjtu OP @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 之前执行。。。。 |
10
lolizeppelin 2018-12-14 16:31:58 +08:00
asyncio 的不知道
具体怎么做看自己需求,常见的两种 1. release 的时候创建一个新的协程, 这个协程的内容是 switch 到 lock 的协程 这样当前协程会继续执行剩下代码.lock 的协程排序 key 是当前的时间点, 调度排位会在前面因为 io 切换的协程之后 2 release 的时候创建一个新的协程, 这个协程的内容是 switch 到当前协程, 然后立即切换到 lock 的协程 这样 lock 的协程会直接被激活, 当前协程剩余代码被调度到以后再继续执行 asyncio 常规采用那种看他代码怎么写的就是 |
11
ucun 2018-12-14 18:40:54 +08:00
|