不死心,再来问一遍关于 Python 的 asyncio 问题

2019-07-22 17:42:42 +08:00
 waibunleung

有两段代码,是关于 asyncio 的。
代码段一:

import asyncio

async def worker_1():
    print('worker_1 start')
    await asyncio.sleep(1)
    print('worker_1 done')

async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(2)
    print('worker_2 done')

async def main():
    print('before await')
    await worker_1()
    print('awaited worker_1')
    await worker_2()
    print('awaited worker_2')

%time asyncio.run(main())

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

before await
worker_1 start
worker_1 done
awaited worker_1
worker_2 start
worker_2 done
awaited worker_2
Wall time: 3 s

代码段二:

import asyncio

async def worker_1():
    print('worker_1 start')
    await asyncio.sleep(1)
    print('worker_1 done')

async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(2)
    print('worker_2 done')

async def main():
    task1 = asyncio.create_task(worker_1())
    task2 = asyncio.create_task(worker_2())
    print('before await')
    await task1
    print('awaited worker_1')
    await task2
    print('awaited worker_2')

%time asyncio.run(main())

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

before await
worker_1 start
worker_2 start
worker_1 done
awaited worker_1
worker_2 done
awaited worker_2
Wall time: 2.01 s

问题:代码段一里面的协程(coroutine)换成代码段二的任务(task)后,为什么执行顺序就变了?这个过程中发生了什么事情?

说说我的猜想:
发现调用 asyncio.run(main()) 或者 [asyncio.gather()->asyncio.get_event_loop()->loop.run_until_complete()]都会将一个 coroutine 转化成 task/future 再放 event loop 里面去, 交由 event loop 去管理这些 task/future。代码段一只将 main()这个 coroutine 封装成了 task 加入到 event loop 中,所以整个 event loop 中只有一个 task 在走,在这个 task 中代码是顺序执行的,所以最后呈现出同步执行的结果;
但是代码段二调用了两次 asyncio.create_task(),这个方法会将一个 coroutine 转换成一个 task 并且放到 event loop 中,所以整个 event loop 其实有三个 task ( main,task1,task2 ),之后程序就交给 event loop 来调度,执行顺序就变不同了。 这个假设目前来看好像能解释得通

最后,希望各位能指点一下~

6247 次点击
所在节点    Python
90 条回复
waibunleung
2019-07-22 17:46:56 +08:00
顺便贴一下之前的问题 https://www.v2ex.com/t/583601#reply46
cwjokaka
2019-07-22 17:55:30 +08:00
好奇,先收藏
BBCCBB
2019-07-22 18:01:34 +08:00
你代码 1 是顺序执行的,
代码 2 加入 create_task 实际上已经把 worker1 和 worker2 这两个函数创建成 task 加入到了 eventLoop 里,你 worker1 函数里有 await sleep(1) , 让出 cpu 后 eventLoop 会继续调度下一个,这里基本就是 worker2. 所以第二个看起来像并行的, 这里的并行是指你 worker1 里主动让出了 cpu, 所以 worker2 在你 worker1 sleep 的过程中可以运行了。

大概应该就是这个道理
BBCCBB
2019-07-22 18:02:20 +08:00
create_task 创建后就加入到了调度器,就会被执行, 不是说等你 await 他的时候他才执行,await 只是等待他执行完成。
zdnyp
2019-07-22 18:05:31 +08:00
zdnyp
2019-07-22 18:06:33 +08:00
@BBCCBB 这是单线程吧...应该没有让出 CPU 的操作吧...
ipwx
2019-07-22 18:09:47 +08:00
@zdnyp 你以为协程是为什么被发明出来的?就是为了单线程里面,应用程序层面上,可以最大效率地执行多个可能阻塞(比如 io 和 sleep )的任务,避免 cpu 空置呀。所以让出 cpu 这个说法很准确
waibunleung
2019-07-22 18:14:34 +08:00
@BBCCBB 你说的话不就是我的猜想么?另外你也是跟我一样的猜想,实际上我稍微去找过源码,我只看到了函数 create_task 里面的注释说将协程加入 event loop 调度并返回一个 task 对象,但是我并没有找到将协程加入 event loop 的代码在哪里。
另外你可以解释一下为什么代码 1 是顺序执行的吗?跟代码 2 对比起来不同的原因是什么?
BBCCBB
2019-07-22 18:16:33 +08:00
我都没看你的猜想 =_=
BBCCBB
2019-07-22 18:18:45 +08:00
你代码 1 里明显是一个一个创建的 task。你又没有显示的加入到 eventloop, 那就只能是在 await 的时候才加进去执行


@zdnyp 我是说这个 task 让出 cpu,让其他的 task 执行。。
so1n
2019-07-22 18:20:23 +08:00
@zdnyp 协程就是非阻塞,处理 io 时会通知协程,协程休眠,不占用 cpu 等到 io 结束协程在占用 cpu,这一步可以说让出 cpu
waibunleung
2019-07-22 18:21:24 +08:00
@ipwx 所以你可以解释一下吗~为什么将协程换成 task 之后执行顺序就不一样了?
guokeke
2019-07-22 18:24:47 +08:00
代码 2 执行顺序不一样是因为 create_task 的两个 task 运行不相互阻塞。
https://github.com/python/asyncio/blob/master/asyncio/base_events.py
create_task:227
call_soon: 562
waibunleung
2019-07-22 18:24:47 +08:00
@BBCCBB 首先代码一里面只有 main()这个主异步函数会被转换成 task,其他的还是 coroutine,另外 create_task 调用之后,转换出来的 task 也没有明显加入 event loop,不过你能找到加进去 event loop 的代码的话,更能支持你的说法...
so1n
2019-07-22 18:26:28 +08:00
create_task 相当于之前的 ensure_future,实际上就是把任务放入 event_loop,并安排他执行。asynico 3.4 之前是 python 编写的,3.7 应该都是 c 了
so1n
2019-07-22 18:28:00 +08:00
waibunleung
2019-07-22 18:28:21 +08:00
@guokeke 我翻源码的时候也看到了这里,但是 create_task 这个函数看上去只是返回了创建的 task 对象,这个函数的注释是"""Schedule a coroutine object.
Return a task object.
"""
请问 Schedule a coroutine object.怎么体现出来?在哪里 schedule 的?
BBCCBB
2019-07-22 18:30:07 +08:00
create_task 返回的是已经被加入到调度器的 task, 你不用 await, 也可以直接用 task.done(), task.result()等方法来获取该 task 执行的结果。

而且你对代码 1 里只有 main()这个函数会被转换成 task 这个理解也不正确,async 函数 也会被创建成 task 加入到 eventloop 中。 eventloop 是不会直接执行 async 函数的
so1n
2019-07-22 18:31:13 +08:00
同时 3.7 支持 asyncio.all_tasks 来返回事件循环所运行的未完成的 Task 对象的集合 和 asyncio.current_task 返回当前运行的 Task 实例你可以自己调试下
guokeke
2019-07-22 18:31:54 +08:00
@waibunleung 你要再深入 tasks.py 那个文件去看,然后又会回到这个 baseEvent 下的 call_soon 函数,中点不是返回了什么,而是 Task 在初始化的时候做了什么。

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

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

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

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

© 2021 V2EX