python3.7 中的 async/await 以及 asyncio 问题

2019-07-16 23:36:22 +08:00
 waibunleung

有一段代码:

import asyncio

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(url))

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    for task in tasks:
        await task

%time asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))

########## 输出 ##########

crawling url_1
crawling url_2
crawling url_3
crawling url_4
OK url_1
OK url_2
OK url_3
OK url_4
Wall time: 3.99 s

我想问在 main 函数中的 for 循环处,原意是等待所有任务结束。但是遇到第一个 await 时不会直接跳出整个 for 循环吗?还是说只是会跳过当前的一轮循环?还是说 for 循环对于 await 其实有特别的处理对待?

我也知道这个和 python 的事件循环有关系,但是在网上找了不少资料都没有很能说清楚个大概的,希望 v 友们能给我解个惑,python 的事件循环是怎么样的?

5685 次点击
所在节点    Python
48 条回复
Torpedo
2019-07-17 10:14:39 +08:00
楼主,你不写 for,tasks 也会执行。
await 只是等一个异步执行完成,至于这个异步什么时候开始,和 await 没关系
tisswb
2019-07-17 10:15:01 +08:00
await 不会跳出循环,而是告诉程序 task,你去干活吧,做完了跟我说,我也要忙别的了。
tisswb
2019-07-17 10:18:10 +08:00
补充一下,就算你不写 for 循环,task 也还是执行的,但是这种情况下,main 结束的话 task 就会被终止,有两种方法解决,1 )手写延迟固定时间; 2 )使用 await,让 main 等各个 task 出结果,都结束在结束自己。
waibunleung
2019-07-17 10:31:07 +08:00
@metaclass 按照你这么说是 blocking 的话,那最后的运行时间应该大于 4s 才对,但是很明显运行时间取决于耗时最大的那个任务
congeec
2019-07-17 10:35:01 +08:00
for loop 并不是 event loop
waibunleung
2019-07-17 10:37:36 +08:00
至于为什么我觉得他会跳出整个 for 循环或者觉得 for 循环对协程会有特别处理,是因为我类比了 nodejs 的 await/async 机制...另外大家有关于 python 的 eventloop 相关介绍吗?
Vegetable
2019-07-17 10:42:28 +08:00
@keepeye 强行 WaitGroup,这样的缺点是需要在写任务的时候就开始考虑并发执行的问题,如果是同一个函数还好,不同类型的任务不在一个函数里定义,不方便 wg.done(),就需要再包装一次,所以还是 asyncio.gather 好一点
xxxy
2019-07-17 10:42:42 +08:00
```
async function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}

async function f(i) {
console.log(`crawl ${i}`)
await sleep(1000)
console.log(`ok ${i}`)
}


async function f1() {
for (let i=0;i<5;i++){
await f(i)
}
}

f1()
```
谁能解释下为什么楼主的程序跟 js 的这段运行结果不一样吗?
waibunleung
2019-07-17 10:45:45 +08:00
@wwqgtxx
> 你这里的 await 其实内部是转化为 yield from 了,但是这个机制是给 asyncio 的 eventloop 使用的,在你 await 的时候会把控制权给别的 task,当别的 task 出现 await 或者说执行完成的时候再回到这个地方接着执行(会恢复现场),直到你当前 tast 结束( return 或者是抛异常)

这么说的话,在 for 循环中遇到 await 时,for 循环所在的主协程会挂起去执行别的 task,那这个时候整个 for 循环会被 block 住不会往下继续执行吧?等到所有任务完成或者 await 之后才往下面执行 for 循环后面的代码?
waibunleung
2019-07-17 10:46:21 +08:00
@congeec 在?既然要评论,就将话说得更具体一些咯
waibunleung
2019-07-17 10:51:43 +08:00
@Vegetable 所以按照你的意思,for 循环里 await,它会阻塞当前正在 await 的任务直到它完成才进到下一轮循环去?
congeec
2019-07-17 11:11:28 +08:00
````
tasks = [task1, task2, task2]
for t in tasks:
await t
```

完全等价于

````
tasks = [task1, task2, task2]
await tasks[0]
await tasks[1]
await tasks[2]
```
这样执行顺序是同步的你能理解吧。其实并不能,因为你可能不知道 Task/Future 和 coroutine 的区别。task 被创建的那一刻就已经开始执行了,你 await 的只不过是他的结果 Task.result()。所以如果你加副作用,比如说 print(),打印出来的结果可能是乱序的。

coroutine 就不一样

```
coros = [coro1, coro2, coro3]
await corps[0]
await corps[1]
await corps[3]
```
这三个 corotines 绝对是按顺序执行


好了,再来说 for loop 和 event loop。
你把 for loop 展开就是几个普通的语句放在一起,没啥好说的

有意思的是 event loop。看下面这些代码。
```
async def coro1:
await asyncio.sleep(xxx)
await asyncio.sleep(xxx)

sync def coro2:
await asyncio.sleep(xxx)
await asyncio.sleep(xxx)

asyncio.gather(coro1, coro2)
```

这儿有两个 coroutines,哪个先执行完呢?不知道。每个 await 那儿都会让出( yield from 的语法糖嘛)控制权。python 知道的是每个 coroutine 的状态( Ready/NotReady )。event loop 会不断的轮询( polls )这些 coroutines,如果状态是 NotReady,就去看看其他的 coroutine,如果是 Ready 就执行下一行呗。

例子里用了 状态机+polling。具体实现取决于平台,我也不知道。
waibunleung
2019-07-17 11:23:11 +08:00
@wwqgtxx 你可以解释一下我 append 的第二段代码的运行逻辑吗?
waibunleung
2019-07-17 11:26:09 +08:00
@congeec 哥,会说话就多说点昂~ 好像有点眉目了
waibunleung
2019-07-17 11:26:38 +08:00
@waibunleung 能解释一下我 append 的第二段代码是逻辑吗?
lolizeppelin
2019-07-17 12:48:34 +08:00
这问题论坛上是问不清楚的.

你真要搞懂直接把 eventlet 的源码读懂就明白了

所有的异步都一个卵模型,套其他语言有是一样

你可以简单理解为所有的异步语法都是生成一个"微线程"被丢到调度队列里
await 语法导致你当前代码块立刻被挂起(变成"微线程"),然后切换到主循环里去了,主循环按照队列的顺序选择执行的“微线程”
切换回来的时候就是你 await 对象完成的时候

说白了都是排序,所有的任务都到队列里排序,等待被调度,整个异步循环就是不停的 goto 来 goto 去,从一个代码片段跳到另外一个片段
wwqgtxx
2019-07-17 12:51:51 +08:00
@Vegetable #19 没有任何好处,只不过可以作为底层实现的一种方式,gather 内部是创建了一个新的 future 配合 done_callback 来解决这个问题
wwqgtxx
2019-07-17 12:55:26 +08:00
@waibunleung 对于协程来说,本来就是只有在 await 的时候才会把当前 task 阻塞,并执行其他 task,或者当前 task return 了
waibunleung
2019-07-17 13:00:52 +08:00
@wwqgtxx 你可以解释一下我 append 的第二段代码的运行逻辑吗?为什么两次打印会在最后才出现?
wwqgtxx
2019-07-17 14:28:44 +08:00
@waibunleung 没有问题呀,await 是等待另一个 task 结束,并不是等待另一个 task 阻塞

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

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

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

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

© 2021 V2EX