关于python3 的asyncio

事出有因

先来看两段关于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/task2 换成 await asyncio.sleep(1) 也是可以的,道理一样,下面会讲
    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的执行过程

  • 先直接看代码二,整个函数的运行从asyncio.run(main())这里开始的。
    1. 跳进去asyncio.run()这个函数可以看到它首先创建了一个event loop(事件循环),这是整个asyncio的基础,没有事件循环就没有办法很好地调度各个协程。
    2. 然后调用了loop.run_until_complete()并将mian这个主协程传递进去函数。
    3. run_until_complete()位于base_events.py,函数有句注释:
      Run until the Future is done.If the argument is a coroutine, it is wrapped in a Task.
      所以函数会调用tasks.ensure_future()来将一个coroutine转化为task,经过一些处理(绑定协程完成时的回调函数)之后就进入到事件循环的核心函数 run_forever()
    4. run_forever()函数中比较关键的一句代码是self._run_once()。跳进去这个_run_once()函数会发现它会从self._ready这个放着准备要执行的task/future队列中拿出一个task(self._ready.popleft()),通过handle._run()来执行该task。

上面的过程是ayncio的事件循环如何启动一个协程执行的过程。

  • 再来看代码段二中main()函数的主要内容

    1. 进到main()后直接调用了asyncio.create_task()将worker_1和worker_2两个coroutine转化成了task对象。
    2. 跳进去asyncio.create_task()函数查看(最后trace back到base_events.py中的create_task()),关键的一句代码是task = tasks.Task(coro, loop=self),这里将一个协程包装成了一个任务。
    3. 去查看tasks.Task 这个类的初始化过程,里面关键的一句代码是:self._loop.call_soon(self.__step, context=self._context)。可以看到self.__step这个函数被作为参数传递了进去。查看self.__step这个函数可以发现,它会获取当前要执行的协程并通过result = coro.send(None)来触发协程执行。
    4. 看回self._loop.call_soon()函数,它里面又调用了_call_soon()函数将__step函数帮装成了event.Handle()对象,并将这个对象放进了self._ready这个 用来存放即将可以执行的Handle对象的队列 里。也即加入到了下一轮时间循环中。
    5. 至此,coroutine通过调用asyncio.create_task()而转化成task,进而会被加入到事件循环中。
  • 然后大概说一下代码二中main()函数的执行

    1. 调用 asyncio.run(main()) 后 main() 这个coroutine会被加入到事件循环并被执行。然后进到main()函数中,worker_1()、worker_2()通过asyncio.create_task()由coroutine转化为task1、task2并被加入到事件循环中。
    2. main()这个协程在执行到 await task1后被挂起,让出控制权,由事件循环调度决定执行去执行task1还是task2。 作为事件循环,当 await task1 把控制权交还的时候,这一轮循环已经结束,随后立即开始下一轮的事件循环,在下一轮循环中检查有没有其他任务(task1和task2)
    3. 其实可以用一个比较简单的方式解释,await 导致了 task 切换,但是 create_task 之后这个 task 已经放进等待列表,所以在 main task 中 await 任何 task 都会导致 worker 被执行,至于 worker1 和 worker2 的执行顺序,就交由事件循环去调度了。这也是在解释代码二里的注释为什么await task1 也可以换成 await asyncio.sleep(1)
  • 至于代码段一

    1. 代码段一中的main()函数没有将worker_1()、worker_2()转化成task,也即没有先加入到事件循环中。
    2. 代码执行到await worker_1()的时候才会把worker_1()这个协程加入到事件循环中并且执行,在执行过程中遇到await asyncio.sleep(1),然而此时循环中也没有别的可执行任务,于是就干等了。await worker_2()的情况类似。
    3. 所以代码一的写法跟直接用同步的方式写没有什么区别。它只是用异步的写法写了一个同步执行的代码。

还有一点点

行文至此想说的能说的基本上都差不多了,如果有什么不对的地方还请多多指正。
另外在探究这个问题的时候提了一些问题以及得到了不少v友的提点,大家如果去看看下面两个链接的话可能会更有帮助,是我的两个提问帖子。

帖子一传送门(python3.7 中的 async/await 以及 asyncio 问题)
帖子二传送门(不死心,再来问一遍关于 Python 的 asyncio 问题)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值