likes
comments
collection
share

Python Asyncio 初探

作者站长头像
站长
· 阅读数 37

前言

许多 Web 应用依赖大量的 I/O (输入/输出) 操作,比如从网站上下载图片、视频等内容;进行网络聊天或者针对后台数据库进行多次查询。数据库查询可能会耗费大量时间,尤其是在该数据库处于高负载或查询很复杂的情况下。 Web 服务器可能需要同时处理数百或数千个请求。

I/O 是指计算机的输入和输出设备,例如键盘、硬盘驱动器,以及最常见的网卡。这些操作等待用户输入或从基于 Web 的 API 检索内容。

Asynchronous IO (async IO) 是一种异步编程设计,并在 Python 3.4 的 asyncio 模块中得到了支持,作为在多线程和多进程之外处理这些高并发工作负载的另一种方法,可以显着提高使用 I/O 操作的应用程序的性能和资源利用率。

什么是同步编程

同步编程,通常来说,大多数编程语言都是子例程调用模型:按照顺序运行代码。在此模型中,下一行代码在前一行代码完成后立即运行,并且一次只完成一个模块。

该模型适用于大部分应用程序。但是,也存在明显的缺点,如果一行代码特别慢怎么办?

在这种情况下,速度慢的代码将导致所有其他代码都将被卡住,直到该行完成。最差的情况下可能导致整个应用程序卡死。可能大多数人在某些软件操作中,一个小小的操作导致整个系统执行不下去,最后只能重启。

什么是异步编程

为了解决同步模型的问题,引入了异步编程的概念,意味着允许同一时刻执行多个任务。

异步编程模型意味着需要长时间运行的任务可以在后台运行,与主应用程序分开。系统可以自由地执行不依赖于该任务的其他工作,而不是阻止所有其他应用程序代码等待该长时间运行的任务完成。然后,一旦长时间运行的任务完成,我们会收到通知它已完成。

asyncio 库允许我们使用异步编程模型运行代码。 这让我们可以一次处理多个 I/O 操作,同时仍然允许我们的应用程序保持响应。

在 Python 3.4 中,asyncio 库中包含了装饰器和生成器 yield from 来定义协程(coroutine)。协程是一种方法,当我们有一个可能长时间运行的任务时可以暂停,然后在该任务完成时恢复。

协程执行完成后返回到调用者有一种新方法:通过 yield 控制。当协程的 yield 执行完成后立即回到了调用点,但是对协程的再次调用不会在起始处再次开始,相反,他们继续从最近停止处继续进行。

如下图所示:

Python Asyncio 初探

def filter_even(numbers):

    for num in range(numbers):
        if (num % 2 == 0):
            yield num

even_number = filter_even(100)
print(list(even_number))

运行结果:

$ python yielddemo.py 
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 
62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98]

ayncio 版 Hello 程序

Python 3.5 版中,当关键字 async 和 await 显式添加到语言中时,该语言实现了对协程和异步编程的一流支持。 这种语法在 C# 和 JavaScript 等其他编程语言中很常见,它允许我们使异步代码看起来像是同步运行的。 这使得异步代码易于阅读和理解,因为它看起来像大多数软件工程师熟悉的顺序流程。 asyncio 是一个使用称为单线程事件循环的并发模型以异步方式执行这些协程的库。

利用 async/await 两个定义关键字定义协程,通过 asyncio 提供运行和管理协程的基础:

import asyncio
import time

async def main():
    print(f'{time.ctime()} Hello!')
    
    await asyncio.sleep(1.0)
    print(f'{time.ctime()} See you again!')

asyncio.run(main())

运行结果:

$ python asynciodemo.py 
Sat Jul  9 23:19:40 2022 Hello!
Sat Jul  9 23:19:41 2022 See you again!

asyncio 提供了一个 run() 函数来执行 async def 函数,然后从那里调用的所有其他协程,如 main() 函数中的 sleep() 函数。asyncio 不是多线程或多进程,而是并行运行代码。

JavaScript 中支持异步执行(浏览器,Nodejs,Electron 等)。在早期版本中,他们只是使用回调功能在异步操作完成后运行其他功能。

如何使用 asyncio

创建协程很简单,与创建普通的 Python 函数没有太大区别。唯一的区别是,我们不是使用 def 关键字,而是 使用 async def 。async 关键字将函数标记为协程,而不是普通的 Python 函数。

import asyncio
import time

def write(msg):
    print(msg, flush=True)

async def say1():
    await asyncio.sleep(1)
    write("Hello from 1")

async def say2():
    await asyncio.sleep(1)
    write("Hello from 2")
    

write("start")
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
    say1(),
    say2()
))

write("exit")

loop.close()

运行该代码,可以看到 Hello from 1 运行 1 秒后运行 Hello from 2

$ python asyncoidemo2.py 
start
Hello from 1
Hello from 2
exit

run_until_complete 运行 say() 函数,解释器会逐行执行该函数的内容。当碰到 await 之后,解释器开始异步操作:这个操作为了循环将完成一些内部回调操作,这个回调操作是对开发人员隐藏的。但是现在,say1 开始后,它立即将控制返回到事件循环。所以,它启动异步 sleep 和控制循环,然后循环实际上已经开始启动 say2 函数。

当第一次异步 sleep 运行 1秒后,进入内部回调执行 say1 协程,下一个操作是打印 Hello from 1。打印后,它再次返回到活动循环。同时,从第二次睡眠开始,循环获得了有关完成第二次睡眠的事件。

所以接下来 Hello from 2 打印,然后第二种方法也返回。 run_until_complete(gather(l1,l2,l3)) 将阻止所有 l1,l2,l3 Coroutines:

Python Asyncio 初探

请注意,7 和 9 事件可能会交换 - 如果您多次运行代码,您可能会注意到 Hello from 1 打印在 Hello from 2 之后。

  • event_loop 事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
  • coroutine 协程:协程对象,指一个使用 async 关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态
  • future: 代表将来执行或没有执行的任务的结果。它和 task 上没有本质上的区别
  • async/await 关键字:python3.5 用于定义协程的关键字,async 定义一个协程,await 用于挂起阻塞的异步调用接口。

总结

本文首先介绍了同步编程和异步编程的概念,然后引出了协程的基本概念,写了 asyncio 版的 HelloWorld 程序,最后给出了 Python 中 asyncio 库的简易使用方法。

协程的优势在于多 IO 操作时能够有效提高程序速度,例如某些 HTTP 客户端,例如 aiohttps 调用服务器中就利用上了 asyncio 库。

参考链接:

转载自:https://juejin.cn/post/7122764568547786783
评论
请登录