中国领先的IT技术网站
|
|

Python中的异步编程:Asyncio

如果你已经决定要理解 Python 的异步部分,欢迎来到我们的“Asyncio How-to ”。哪怕连异动范式的存在都不知道的情况下,你也可以成功地使用 Python。但是,如果你对底层运行模式感兴趣的话,asyncio 绝对值得查看。

作者:佚名来源:Python开发者|2017-08-02 15:00

【51CTO活动】8.26 带你与清华大学、搜狗、京东大咖们一起探讨基于算法的IT运维实践


如果你已经决定要理解 Python 的异步部分,欢迎来到我们的“Asyncio How-to ”。

注:哪怕连异动范式的存在都不知道的情况下,你也可以成功地使用 Python。但是,如果你对底层运行模式感兴趣的话,asyncio 绝对值得查看。

异步是怎么一回事?

在传统的顺序编程中, 所有发送给解释器的指令会一条条被执行。此类代码的输出容易显现和预测。 但是…

譬如说你有一个脚本向3个不同服务器请求数据。 有时,谁知什么原因,发送给其中一个服务器的请求可能意外地执行了很长时间。想象一下从第二个服务器获取数据用了10秒钟。在你等待的时候,整个脚本实际上什么也没干。如果你可以写一个脚本可以不去等待第二个请求而是仅仅跳过它,然后开始执行第三个请求,然后回到第二个请求,执行之前离开的位置会怎么样呢。就是这样。你通过切换任务最小化了空转时间。尽管如此,当你需要一个几乎没有I/O的简单脚本时,你不想用异步代码。

还有一件重要的事情要提,所有代码在一个线程中运行。所以如果你想让程序的一部分在后台执行同时干一些其他事情,那是不可能的。

准备开始

这是 asyncio 主概念最基本的定义:

  • 协程 — 消费数据的生成器,但是不生成数据。Python 2.5 介绍了一种新的语法让发送数据到生成器成为可能。我推荐查阅David Beazley “A Curious Course on Coroutines and Concurrency” 关于协程的详细介绍。
  • 任务 — 协程调度器。如果你观察下面的代码,你会发现它只是让 event_loop 尽快调用它的_step ,同时 _step 只是调用协程的下一步。
  1. class Task(futures.Future):   
  2.  
  3.     def __init__(self, coro, loop=None): 
  4.  
  5.         super().__init__(loop=loop) 
  6.  
  7.         ... 
  8.  
  9.         self._loop.call_soon(self._step) 
  10.  
  11.   
  12.  
  13.     def _step(self): 
  14.  
  15.             ... 
  16.  
  17.         try: 
  18.  
  19.             ... 
  20.  
  21.             result = next(self._coro) 
  22.  
  23.         except StopIteration as exc: 
  24.  
  25.             self.set_result(exc.value) 
  26.  
  27.         except BaseException as exc: 
  28.  
  29.             self.set_exception(exc) 
  30.  
  31.             raise 
  32.  
  33.         else
  34.  
  35.             ... 
  36.  
  37.             self._loop.call_soon(self._step)  
  • 事件循环 — 把它想成 asyncio 的中心执行器。

现在我们看一下所有这些如何融为一体。正如我之前提到的,异步代码在一个线程中运行。

从上图可知:

1.消息循环是在线程中执行

2.从队列中取得任务

3.每个任务在协程中执行下一步动作

4.如果在一个协程中调用另一个协程(await <coroutine_name>),会触发上下文切换,挂起当前协程,并保存现场环境(变量,状态),然后载入被调用协程

5.如果协程的执行到阻塞部分(阻塞I/O,Sleep),当前协程会挂起,并将控制权返回到线程的消息循环中,然后消息循环继续从队列中执行下一个任务...以此类推

6.队列中的所有任务执行完毕后,消息循环返回第一个任务

异步和同步的代码对比

现在我们实际验证异步模式的切实有效,我会比较两段 python 脚本,这两个脚本除了sleep 方法外,其余部分完全相同。在第一个脚本里,我会用标准的 time.sleep 方法,在第二个脚本里使用 asyncio.sleep 的异步方法。

这里使用 Sleep 是因为它是一个用来展示异步方法如何操作 I/O 的最简单办法。

使用同步 sleep 方法的代码:

  1. import asyncio   
  2.  
  3. import time   
  4.  
  5. from datetime import datetime 
  6.  
  7.   
  8.  
  9.   
  10.  
  11. async def custom_sleep():   
  12.  
  13.     print('SLEEP', datetime.now()) 
  14.  
  15.     time.sleep(1) 
  16.  
  17.   
  18.  
  19. async def factorial(name, number):   
  20.  
  21.     f = 1 
  22.  
  23.     for i in range(2, number+1): 
  24.  
  25.         print('Task {}: Compute factorial({})'.format(name, i)) 
  26.  
  27.         await custom_sleep() 
  28.  
  29.         f *= i 
  30.  
  31.     print('Task {}: factorial({}) is {}\n'.format(name, number, f)) 
  32.  
  33.   
  34.  
  35.   
  36.  
  37. start = time.time()   
  38.  
  39. loop = asyncio.get_event_loop() 
  40.  
  41.   
  42.  
  43. tasks = [   
  44.  
  45.     asyncio.ensure_future(factorial("A", 3)), 
  46.  
  47.     asyncio.ensure_future(factorial("B", 4)), 
  48.  
  49.  
  50. loop.run_until_complete(asyncio.wait(tasks))   
  51.  
  52. loop.close() 
  53.  
  54.   
  55.  
  56. end = time.time()   
  57.  
  58. print("Total time: {}".format(end - start))  

脚本输出:

  1. Task A: Compute factorial(2)   
  2.  
  3. SLEEP 2017-04-06 13:39:56.207479   
  4.  
  5. Task A: Compute factorial(3)   
  6.  
  7. SLEEP 2017-04-06 13:39:57.210128   
  8.  
  9. Task A: factorial(3) is 6 
  10.  
  11.   
  12.  
  13. Task B: Compute factorial(2)   
  14.  
  15. SLEEP 2017-04-06 13:39:58.210778   
  16.  
  17. Task B: Compute factorial(3)   
  18.  
  19. SLEEP 2017-04-06 13:39:59.212510   
  20.  
  21. Task B: Compute factorial(4)   
  22.  
  23. SLEEP 2017-04-06 13:40:00.217308   
  24.  
  25. Task B: factorial(4) is 24 
  26.  
  27.   
  28.  
  29. Total time: 5.016386032104492  

使用异步 Sleep 的代码:

  1. import asyncio   
  2.  
  3. import time   
  4.  
  5. from datetime import datetime 
  6.  
  7.   
  8.  
  9.   
  10.  
  11. async def custom_sleep():   
  12.  
  13.     print('SLEEP {}\n'.format(datetime.now())) 
  14.  
  15.     await asyncio.sleep(1) 
  16.  
  17.   
  18.  
  19. async def factorial(name, number):   
  20.  
  21.     f = 1 
  22.  
  23.     for i in range(2, number+1): 
  24.  
  25.         print('Task {}: Compute factorial({})'.format(name, i)) 
  26.  
  27.         await custom_sleep() 
  28.  
  29.         f *= i 
  30.  
  31.     print('Task {}: factorial({}) is {}\n'.format(name, number, f)) 
  32.  
  33.   
  34.  
  35.   
  36.  
  37. start = time.time()   
  38.  
  39. loop = asyncio.get_event_loop() 
  40.  
  41.   
  42.  
  43. tasks = [   
  44.  
  45.     asyncio.ensure_future(factorial("A", 3)), 
  46.  
  47.     asyncio.ensure_future(factorial("B", 4)), 
  48.  
  49.  
  50. loop.run_until_complete(asyncio.wait(tasks))   
  51.  
  52. loop.close() 
  53.  
  54.   
  55.  
  56. end = time.time()   
  57.  
  58. print("Total time: {}".format(end - start))  

脚本输出:

  1. Task A: Compute factorial(2)   
  2.  
  3. SLEEP 2017-04-06 13:44:40.648665 
  4.  
  5.   
  6.  
  7. Task B: Compute factorial(2)   
  8.  
  9. SLEEP 2017-04-06 13:44:40.648859 
  10.  
  11.   
  12.  
  13. Task A: Compute factorial(3)   
  14.  
  15. SLEEP 2017-04-06 13:44:41.649564 
  16.  
  17.   
  18.  
  19. Task B: Compute factorial(3)   
  20.  
  21. SLEEP 2017-04-06 13:44:41.649943 
  22.  
  23.   
  24.  
  25. Task A: factorial(3) is 6 
  26.  
  27.   
  28.  
  29. Task B: Compute factorial(4)   
  30.  
  31. SLEEP 2017-04-06 13:44:42.651755 
  32.  
  33.   
  34.  
  35. Task B: factorial(4) is 24 
  36.  
  37.   
  38.  
  39. Total time: 3.008226156234741  

从输出可以看到,异步模式的代码执行速度快了大概两秒。当使用异步模式的时候(每次调用 await asyncio.sleep(1) ),进程控制权会返回到主程序的消息循环里,并开始运行队列的其他任务(任务A或者任务B)。

当使用标准的 sleep方法时,当前线程会挂起等待。什么也不会做。实际上,标准的 sleep 过程中,当前线程也会返回一个 python 的解释器,可以操作现有的其他线程,但这是另一个话题了。

推荐使用异步模式编程的几个理由

很多公司的产品都广泛的使用了异步模式,如 Facebook 旗下著名的 React Native 和 RocksDB 。像 Twitter 每天可以承载 50 亿的用户访问,靠的也是异步模式编程。所以说,通过代码重构,或者改变模式方法,就能让系统工作的更快,为什么不去试一下呢?

【编辑推荐】

  1. 浅析Python的类、继承和多态
  2. 2017,最受欢迎的15大Python库有哪些?
  3. 给Python小白看的10个使用案例,入门Python就在这里了
  4. Python Decorator基础
  5. Python Yield Generator详解
【责任编辑:枯木 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

Head First 设计模式(中文版)

本书共有14章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部23个设计模式。前言先介绍这本书的用法;第1章到第11章陆续介绍的设...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× 官方软考报名与培训中心