asyncio常见误区

现在到了python3.8,标准库中有很多方式可以去实现,异步并发的编程模式,比如

当然还有一些优秀第三方库,也是不错的选择,这些库都是python2里的佼佼者,比如

asyncio

asyncio is a library to write concurrent code using the async/await syntax.

https://docs.python.org/3.8/library/asyncio.html

asyncio是python3.4引入的一个新的标准库,用来实现协程相关功能。不同于传统多进程多线程的并发方式,asyncio引入eventloop来处理异步任务的调度问题,随着python3新版本逐步完善asyncio成为了python实现异步的重要方案之一

asyncio经历了几个大版本的迭代

  • python3.4 @asyncio.coroutineyield from描述一个异步函数
  • python3.5 引入关键字asyncawait来替代之前装饰器的写法
  • python3.6 核心API更加稳定,可以用于生产环境

asyncio的核心就是一个事件循环,工作时,不停地读取待处理的事件,根据逻辑处理,然后继续循环。事件循环的方式之所以快,是由于相比多线程,减少了系统现成切换的开销,线程相关的比较后面再讨论

使用asyncio开发常见误区

Q:使用asyncio编程,比传统多线程更快

A:asyncio实现的只是另一种网络并发模型,与传统的多线程模型相比,有很多优势,但是正如之前说的asyncio只能节省系统线程调度的开销,asyncio更适合大量IO并发操作的场景

Q:使用async关键字定义的函数就是异步函数,可以异步运行

A:使用async def 定义的函数确实是异步函数,但是是否可以异步执行,还是取决于函数中编写的逻辑,在异步函数中的同步逻辑还是会同步执行,阻塞整个事件循环的运行

基于事件循环的异步模型,在编写时需要显示声明,上下文切换,方便eventloop去调度任务和注册回调,在asyncio中 await 就是用来声明上下文切换的

1
2
async def sleep():
time.sleep(1)

这个函数执行sleep由于没有显示声明,那么在执行的过程中,并不会切换去执行其他操作,整个事件循环会卡主1秒什么都不做,实际上应该这样定义

1
2
async def sleep():
await asyncio.sleep(1)

在这个函数执行的时候,asyncio.sleep会立即返回一个awaitable的对象,这个时候eventloop注册完回调后,可以去处理其他的协程逻辑,1秒后触发回调函数,eventloop回来继续处理后续逻辑,并不影响其他协程工作

理论上每一个async def的函数里面,都应该有await关键字,不然就跟同步函数没有任何区别

Q:那么什么时候需要使用await

A:我们平常说的异步,指的是异步IO

IO常用的分2类,一个是网络IO,一个是磁盘IO

网络IO理解起来很简单,你的程序需要连接网络操作,无论是TCP还是UDP,或者是构建在TCP上的应用协议,比如http mysql redis协议,这些的都可能阻塞程序运行,我们在做任何网络操作的时候都需要考虑应用等待,比如http响应慢,mysql慢查询等

磁盘IO也很好理解,就是程序对磁盘的读写,磁盘并不是立刻响应读写,那么在做读写操作的过程中,都会有等待延迟,目前asyncio并不支持异步磁盘读写

还有一些情况,比如程序逻辑里需要运行shell命令,也不能直接使用传统的subprocess库,应该是使用asyncio.create_subprocess_exec

Q:为什么添加了await会报错,使用的库不支持await怎么办

A:asyncio整体逻辑自成一套,基本上决定了整体生态和之前同步运行的操作库无法直接兼容,asyncio发展这个多年,也诞生了不少满足asyncio标准的操作库,这些库操作方式与之前的库类似,并且支持异步调用,这里列举一些常用的

功能 同步操作库 异步操作库
http请求 requests Aiohttp
mysql操作 MysqlDB pymysql Aiomysql
redis操作 redis aioredis

Q:我需要做一个事情,根本没有异步操作库,只有一个同步的库,怎么办

A:事实上asyncio中提供一种办法在事件循环中执行同步的函数,其原理是封装了一个线程池成一个异步执行对象,在这个线程池中执行同步的函数,然后回调事件循环来处理

1
2
3
4
5
6
7
8
import asyncio
import time

async def sleep_async(loop, delay):
# None 默认使用 ThreadPoolExecutor
await loop.run_in_executor(None, time.sleep, delay)
# 等价于
await asyncio.sleep(delay, loop=loop)

目前市面上也有很多操作库,通过类似的封装来实现将现有同步的库转换成异步库

Q:打日志会阻塞事件循环吗?

A:理论上会的,主要取决于日志去向,如果是打印到标准输入,基本不会,如果打印到文件,理论上会存在磁盘IO阻塞,但是因为一般日志写入都比较快,基本影响不大,但是也存在磁盘故障,导致磁盘IO缓慢的时候影响服务的可能,如果日志后端是网络后端,那么就需要特别注意了,这个网络日志的处理是否使用队列,或者在后台线程处理,如果打日志的时候连接日志后端服务器,那么参考之前说的网络IO的情况

现在很多服务部署的方案结合docker容器,直接输出到标准输出,交给docker处理,处理起来会简单不少,另外极致追求高性能的服务是不会打日志的,或者只打错误日志

Q:我都改造完了为什么我的函数逻辑执行时间还是那么久

A:asyncio帮助我们结局阻塞IO的问题,并不是说减少了阻塞时间,只是帮助程序在阻塞的时候,利用阻塞时间,去做其他的事情,帮助减少整体程序执行时间,帮助提高并发性能,函数逻辑慢,还需要从函数逻辑本身逐步排查