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 的事件循环是怎么样的?

5684 次点击
所在节点    Python
48 条回复
ysc3839
2019-07-16 23:41:17 +08:00
https://docs.python.org/zh-cn/3/library/asyncio-task.html
我估计 await task 不会等待 task 执行完的。
junkun
2019-07-16 23:43:25 +08:00
await 会跳出循环吗? await 不会跳出循环吧。
ysc3839
2019-07-16 23:52:29 +08:00
@junkun 楼主的意思是 await 类似 yield 那种效果吧,在函数中间返回。
reus
2019-07-16 23:55:09 +08:00
await 就是挂起等待,task 执行完,再继续 await 后面的,是不是在 for 循环里都没有任何区别
Vegetable
2019-07-17 00:10:19 +08:00
这代码问题挺大的,我看了很久才看出来他到底是什么意思,可以说属于奇淫技巧。
当然了,我看懂以后就能明白咋回事了。

任务不是在 main 中的 await 里执行的,这里只是在检测任务是不是完成了。create_task 之后,任务就会开始执行,所以 tasks 生成之后就开始执行任务了,作为测试,可以在 for 循环前添加一个 await asyncio.sleep(10)来验证。创建完 tasks 之后使用 for 循环去 await 任务,已经完成的就会进入下一次循环,没完成的会阻塞 for 循环,最后所有任务都完成了才能走完循环结束任务。

我挺不喜欢这个写法的
Vegetable
2019-07-17 00:13:11 +08:00
验证代码
import asyncio

async def task():
print("开始")
await asyncio.sleep(5)
print("结束")

async def main():
tasks = [asyncio.create_task(task()) for i in range(3)]
await asyncio.sleep(10) # 这一行会阻塞程序结束,但是不会影响开始和结束输出
for t in tasks:
await t
asyncio.run(main())
so1n
2019-07-17 00:13:31 +08:00
await 是主动挂起等待,这时要是有别的协程再跑就跑的协程,但你这里没有。for 循环替换成 asynico.wait(task)即可
ClericPy
2019-07-17 00:43:32 +08:00
很多地方协程里 Task 和 Future 的设计复用了(甚至原样用)多线程的 concurrent.futures 那套
Task 类创建以后就开始执行了,Future 则不会
你挨个 await 用来等他们全跑完原则上没什么毛病,不过可能会有一些异常如果不是 return exception 的话会打断 for Loop
所以可以考虑用下原生的 asyncio.wait 等方法来实现
wwqgtxx
2019-07-17 05:22:02 +08:00
@Vegetable 用 create_task 去创建任务而不是直接在 main 中 await 是一种很常见的操作,并不算什么非常规写法,你可以大概类比到多线程编程中开多个子线程然后挨个 wait 它们结束。至于你直接在主 task 中 await 就变成串行执行了,完全改变了程序的本意
wwqgtxx
2019-07-17 05:28:44 +08:00
回答一下楼主的问题,你这里的 await 其实内部是转化为 yield from 了,但是这个机制是给 asyncio 的 eventloop 使用的,在你 await 的时候会把控制权给别的 task,当别的 task 出现 await 或者说执行完成的时候再回到这个地方接着执行(会恢复现场),直到你当前 tast 结束( return 或者是抛异常)
建议楼主先学习一下 python 的生成器,自己用 yield 和 yield from 配合.send()来模仿一下 asyncio 的原理就能深入的了解你想知道的事件循环到底是怎么回事了
metaclass
2019-07-17 06:19:24 +08:00
楼主,你这样写实际上每个 task 之间还是 blocking 的,因为你放到 for 循环里去 await,执行完一个再执行另一个。这个写法是不对的

如果要异步多个 async task,需要用 gather():
https://docs.python.org/3/library/asyncio-task.html#asyncio.gather

asyncio.gather()实际上和 JavaScript 的 Promise.all()类似:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
wwqgtxx
2019-07-17 07:05:35 +08:00
@metaclass 请不要误导人,在 create_task 之后就不是了,请老老实实看 create_task 的说明文档
jaskle
2019-07-17 07:15:26 +08:00
async,await,yield 这不是 js 语法?
wwqgtxx
2019-07-17 07:16:28 +08:00
@metaclass 如果你仔细看过 asycnio.gather 就会发现它的内部调用了 ensure_future
https://github.com/python/cpython/blob/3.7/Lib/asyncio/tasks.py#L746
而 ensure_future 内部会调用 create_task
https://github.com/python/cpython/blob/3.7/Lib/asyncio/tasks.py#L608
关于 create_task 的文档在这里
https://docs.python.org/3/library/asyncio-eventloop.html#creating-futures-and-tasks
其中明确提到了 Schedule the execution of a Coroutines. Return a Task object.
而且楼主的实验也证明了他创建的 task 是交替执行的
wwqgtxx
2019-07-17 07:18:26 +08:00
@jaskle c#也有这种用法,又不是 js 家的专利🙄
wwqgtxx
2019-07-17 07:31:20 +08:00
keysona
2019-07-17 08:25:32 +08:00
当我碰到用 python 的异步 io 出问题的帖子,我都要回复:用 go 吧,python 的异步你用着难受。
keepeye
2019-07-17 08:36:52 +08:00
```
import asyncio

wg = 0

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


async def main(urls):
global wg
for url in urls:
wg+=1
asyncio.ensure_future(crawl_page(url))
while wg > 0:
asyncio.sleep(0.1)
```
Vegetable
2019-07-17 09:44:45 +08:00
@wwqgtxx #9 这是不是应该直接用 asyncio.gather(*task)?我没看出来有什么别的好处,代码也更多
qq976739120
2019-07-17 09:48:05 +08:00
按照我的经验来看...如果不是闲的蛋疼,不要用 asyncio,gevent 一把梭就好

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

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

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

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

© 2021 V2EX