文章目录
Python异步编程学习笔记
-
参考文章:https://www.bilibili.com/video/BV1dD4y127bD?p=2
-
为什么要讲异步编程
- 异步非阻塞、asyncio
- tornado、fastapi、django 3.x asgi、aiohttp都在异步 ->提升性能
-
如何讲解
- 协程
- asyncio模块进行异步编程
- 实战案例
1 协程
协程不是计算机提供,而是程序员人为创造,是一种运行方式
协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块间的相互切换并执行。例如:
def func1():
print(1)
...
print(2)
def func2():
print(3)
...
print(4)
func1()
func2()
# 程序在func1于func2之间来回切换执行,使得结果输入为1 3 2 4
实现协程有那么几种方法:
- greenlet,早期第三方模块
- yield关键字
- asyncio装饰器(py3.4提供的内置模块)
- async、await关键字(py3.5提供的用于代替简化asyncio的关键字)(推荐使用)
1.1 greetlet实现协程
pip install greenlet
from greenlet import greenlet
def func1():
print(1) # 第1步:输出1
gr2.switch() # 第2步,切换到gr2对应的func2函数
print(2) # 第5步,输出2
gr2.switch() # 第6步,切换到func2
def func2():
print(3) # 第3步:输出3
gr1.switch() # 第4步,切换到func1函数
print(4) # 第7步,输出4
gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch() # 第1步:去执行func1函数
# 输出结果为1 3 2 4
1.2 yield关键字
def func1():
yield 1 # 第1步:输出1
yield from func2() # 第2步,切换到func2函数
yield 2 # 第5步,输出2
def func2():
yield 3 # 第3步:输出3
yield 4 # 第4步,输出4
f1 = func1()
for item in f1:
print(item)
# 注意:yield的切换是从函数开头开始
# 输出结果为1 3 2 4
1.3 asyncio装饰器
在python3.4及之后的版本
import asyncio
@asyncio.coroutine
def func1():
print(1)
# ... 这里可以写网络IO请求的代码
yield from asyncio.sleep(2) # 遇到IO耗时操作时,等待两秒,这时程序会自动切换到tasks中的其他任务,两秒过后会继续从当前代码行向下执行
print(2)
@asyncio.coroutine
def func2():
print(3)
# ... 这里可以写网络IO请求的代码
yield from asyncio.sleep(2) # 遇到IO耗时操作时,等待两秒,这时程序会自动切换到tasks中的其他任务,两秒过后会继续从当前代码行向下执行
print(4)
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# 输出结果为1 3 2 4
注意:遇到IO阻塞时自动切换任务
1.4 async、await关键字
在python3.5及之后的版本
async和await关键字的推出是为了简化asyncio的操作,本质还是和asyncio一样
使用async代替@asyncio.coroutine,使用await代替yield from
import asyncio
async def func1():
yield 1
await asyncio.sleep(2)
yield 2
async def func2():
yield 3
yield 4
tasks = [
asyncio.ensure_future(func1()),
asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# 输出结果为1 3 2 4
2 协程意义
不等待IO执行结果。
在一个线程中一个任务遇到了IO等待的话,不等待IO结果,而直接去执行其他任务。
案例:下载三张图片(网络IO)
-
普通方式下载图片
-
import requests import time def download_image(url): print('开始下载:', url) # 发送网络请求,下载图片 response = requests.get(url) # 将图片保存到本地 file_name = url.split('@')[0].rsplit('/')[-1] with open(file_name, 'wb') as file_object: file_object.write(response.content) print('下载完成') if __name__ == '__main__': url_list = ['https://i0.hdslb.com/bfs/archive/edf302b33b922c19c25abcfd751f12ba81f8155f.jpg@880w_388h_1c_95q', 'https://i0.hdslb.com/bfs/archive/d42643d17e3e12075a805217cbd3458258c38b57.png@880w_388h_1c_95q', 'https://i0.hdslb.com/bfs/archive/6a92aac791c1af80fb211145b4a5dc0e1541cbf7.jpg@880w_388h_1c_95q'] start = time.time() for item in url_list: download_image(item) end = time.time() print(end - start)
-
-
协程方式下载图片
-
import aiohttp import asyncio import time async def fetch(session, url): print('发送请求:', url) async with session.get(url, verify_ssl=False) as response: content = await response.content.read() file_name = url.split('@')[0].rsplit('/', 1)[-1] with open(file_name, mode='wb') as file_object: file_object.write(content) print('下载完成') async def main(): async with aiohttp.ClientSession() as session: url_list = ['https://i0.hdslb.com/bfs/archive/edf302b33b922c19c25abcfd751f12ba81f8155f.jpg@880w_388h_1c_95q', 'https://i0.hdslb.com/bfs/archive/d42643d17e3e12075a805217cbd3458258c38b57.png@880w_388h_1c_95q', 'https://i0.hdslb.com/bfs/archive/6a92aac791c1af80fb211145b4a5dc0e1541cbf7.jpg@880w_388h_1c_95q'] tasks = [asyncio.create_task(fetch(session, url)) for url in url_list] await asyncio.wait(tasks) if __name__ == '__main__': start = time.time() asyncio.run(main()) end = time.time() print(end - start)
-
3 异步编程
3.1 事件循环
事件循环可以理解为成一个死循环,该死循环内的功能是不断的检测任务的状态并执行不同的操作,
理解为一个空间,该空间的功能是对放入该空间的任务不断地检测并执行
# 伪代码
任务列表 = [任务1, 任务2, 任务3, ...]
while True:
可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将‘可执行’和‘已完成’的任务返回
for 就绪任务 in 可执行的任务列表:
执行已就绪的任务
for 已完成的任务 in 已完成的任务列表:
在任务列表中移除 已完成的任务
如果 任务列表中的任务都已经完成即没有任务了,则终止循环
import asyncio
# 去生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 将任务放到 `任务列表`
loop.run_until_complete(任务)
3.2 快速上手
协程函数,定义函数时候async def 函数名
协程对象,执行协程函数()得到的协程对象
async def func():
pass
result = func()
注意:执行协程函数创建协程对象,函数内部代码不会执行
想要去运行协程函数内部代码,需要将协程对象交给事件循环来处理。
import asyncio
async def func():
print('冲冲冲')
result = func()
#loop = asyncio.get_event_loop()
#loop.run_until_complete( result )
asyncio.run(result) # python3.7只需要这一句就可以实现上面两句功能
3.3 await
await + 可等待的对象(协程对象、Future、Task对象 -> IO等待)
示例1:
import asyncio
async def func():
print('来玩呀')
response = await asyncio.sleep(2)
print('不玩了')
asyncio.run(func()) # 将协程对象传入事件循环中运行
示例2:
import asyncio
async def others():
print('start')
await asyncio.sleep(2)
print('end')
return '返回值'
async def func():
print(’执行协程函数内部代码) # 第2步
# 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
response = await others() # 第3步 去执行其他的异步函数,一直等待直到等到结果返回
print('IO请求结束,结果为:', response)
asyncio.run(func()) # 第1步,将协程对象传入事件循环
示例3:
import asyncio
async def others():
print('start')
await asyncio.sleep(2)
print('end')
return '返回值'
async def func():
print(’执行协程函数内部代码)
# 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
response1 = await others() # 在这个func函数中一直等待,直到得到others函数返回值,才继续往下执行
print('IO请求结束,结果为:', response1)
response2 = await others()
print('IO请求结束,结果为:', response2)
asyncio.run(func())
await就是等待,等到对象返回结果之后才继续往下走。
3.4 Task对象
Tasks are used to schedule coroutines concurrently.
When a coroutine is wrapped into a Task with functions like
asyncio.create_task()
the coroutine is automatically scheduled to run soon.
白话:Task对象就是将任务添加到事件循环中去或在事件循环中添加多个任务。
Tasks用于并发调用协程,通过asyncio.create_task(协程对象)
的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。除了使用asyncio.create_task()
函数以外,还可以用低层级的loop.create_task()
或 asyncio.ensure_future
函数。不建议手动实例化Task对象
注意:asyncio.create_task()
函数在Python 3.7中被加入。在Python 3.7 之前,可以改用低层级的asyncio.ensure_future()
函数。
示例1:
import asyncio
async def func():
print(1)
await asyncio.sleep(2)
print(2)
return '返回值'
async def main():
print('main开始')
# 创建Task对象,将当前执行的func函数添加到事件循环中
task1 = asyncio.create_task(func())
# 同上,这里create_task函数只是将func函数添加到事件循环中,并不会执行内部代码
task2 = asyncio.create_task(func())
print('main结束')
# 当事件循环中的任务遇到IO操作时,才会自动化切换执行其他任务
# 此处的await是等待相对应的协程全部执行完毕并获取结果
ret1 = await task1
ret2 = await task2
print(ret1,ret2)
asyncio.run( main() )
示例2:
import asyncio
async def func():
print(1)
await asyncio.sleep(2)
print(2)
return '返回值'
async def main():
print('main开始')
task_list = [
asyncio.create_task(func()),
asyncio.create_task(func())
]
print('main结束')
done, pending = await asyncio.wait(task_list, timeout=1)
print('done:', done)
print('pending:', pending)
asyncio.run(main())
示例3:
import asyncio
async def func():
print(1)
await asyncio.sleep(2)
print(2)
return '返回值'
task_list = [
func(),
func()
]
done,pending = asyncio.run(asyncio.wait(task_list))
print(done)
for d in done:
print(d.result())
3.5 asyncio.Future对象
A
Future
is a special low-level awaitable object that represents an eventual result of an asynchronous operation.
Task继承Future,Task对象内部await结果的处理是基于Future对象来的
await 这个关键字功能是一直等待await后面的对象执行完获取到对应的结果,这个一直等待就相当于一个夯住过程,而await关键字是如何实现夯住直到获取到结果的过程呢,其实是通过future对象来实现的。
示例1:
async def main():
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 创建一个任务(Future对象),将这个任务添加进事件循环,这个任务什么都不干
fut = loop.create_future()
# 等待任务最终结果(Future对象),没有结果则会一直等待下去
await fut
asyncio.run( main() ) # 这里已经创建了事件循环,并将main函数添加进去并执行
示例2:
import asyncio
async def set_after(fut):
await asyncio.sleep(2)
fut.set_result('666') # 给fut对象赋值
async def main():
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 创建一个任务(Future对象),将这个任务添加进事件循环,这个任务什么都不干
fut = loop.create_future()
# 创建一个任务(Task对象),绑定了set_after函数,函数内部在2s之后会给fut赋值。
# 即手动设置了future任务的最终结果,那么fut对象被赋值后有值了,await fut就会结束
await loop.create_task( set_after(fut) )
# 等待任务最终结果(Future对象),没有结果则会一直等待下去
data = await fut
print(data)
asyncio.run( main() ) # 这里已经创建了事件循环,并将main函数添加进去并执行
3.6 concurrent.futures.Future对象
使用线程池、进程池实现异步操作时用到的对象
import time
from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures.process import ProcessPoolExecutor
def func(value):
time.sleep(1)
print(value)
# 创建线程池
pool = ThreadPoolExecutor(max_workers=5)
# 创建进程池
# pool = ProcessPoolExecutor(max_workers=5)
for i in range(10):
fut = pool.submit(func, i)
print(fut)
以后写代码可能会存在交叉使用协程和线程/进程。例如:crm项目80%都是基于协程异步编程 ,假设第三方数据库不支持协程async和await来做异步编程,那么第三方数据库工具就需要使用线程、进程做异步编程。
即可以理解为第三方数据库模块使用concurrent.futures.Future对象来做的异步编程,我们需要将这个Future对象转换成asycio.Future对象来融合到crm项目中(因为crm是基于协程来进行异步编程的,协程使用的是asycio.Future对象)。
import time
import asyncio
import concurrent.futures
def func1():
# 某个耗时操作
time.sleep(2)
return '666'
async def main():
loop = asyncio.get_running_loop()
# 1. Run in the default loop's executor(默认 ThreadPoolExecutor)
# 第1步:内部会先调用 TreadPoolExecutor 的 submit 方法去线程池中申请一个线程去执行func1函数,并返回一个concurrent.futures.Future对象
# 第2步:run_in_executor内部会调用asyncio.wrap_future将concurrent.futures.Future对象包装为asyncio.Future对象。
# 因为concurrent.futures.Future对象不支持await语法,所以需要包装为asyncio.Future对象才能使用。
# 注意这里的func1不是协程函数,这里的func1函数相当于第三方数据库函数,它本身不支持协程异步而且它是IO操作的话,就需要使用run_in_executor方法将它变成asyncio.Future对象,变成asyncio.Future对象后就可以使用await去等待,去异步执行这个func1函数
fut = loop.run_in_executor(None, func1)
result = await fut
print('default thread pool', result)
# 2. Run in a custom thread pool:
# with concurrent.futures.ThreadPoolExecutor() as pool:
# result = await loop.run_in_executor(pool, func1)
# print('custom thread pool', result)
# 3. Run in a custom process pool:
# with concurrent.futures.ProcessPoolExecutor() as pool:
# result = await loop.run_in_executor(pool, func1)
# print('custom process pool', result)
asyncio.run(main())
3.7 异步迭代器
普通迭代器:实现了__iter__()
和__next__()
,__iter__()
用于返回迭代器示例本身self,__next__
用于共同完成迭代器的迭代功能。
普通可迭代器对象:必须实现__iter__()
,但不能实现__next__()
,__iter__()
用于实例化一个迭代器对象,即返回一个迭代器。
异步迭代器:实现了__aiter__()
和__anext__()
方法的对象。__anext__
必须返回一个awaitable对象。async for
会处理异步迭代器的__anext__()
方法所返回的可等待对象,直到其引发一个StopAsyncIteration
异常。
异步可迭代对象:可在async for
语句中被使用的对象。必须通过它的__aiter__()
方法返回一个asynchronous iterator。
import asyncio
class Reader(object):
""" 自定义异步迭代器(同时也是异步可迭代对象)"""
def __init__(self):
self.count = 0
async def readline(self):
# await asyncio.sleep(1)
self.count += 1
if self.count == 100:
return None
return self.count
def __aiter__(self):
return self
async def __anext__(self):
val = await self.readline()
if val == None:
raise StopAsyncIteration
return val
async def func():
obj = Reader()
async for item in obj:
print(item)
asyncio.run( func() )
注意:async for 语句只能在协程函数里面使用
3.8 异步上下文管理器
此种对象是通过定义__aenter__()
和__aexit__()
方法来对async with
语句中的环境进行控制。
示例:可以在操作数据库前后通过上下文管理器完成连接数据库操作和关闭连接操作。
import asyncio
class AsyncContextManager:
def __init__(self,conn):
self.conn = conn
async def do_something(self):
# 异步操作数据库
return 666
async def __aenter__(self):
# 异步连接数据库
self.conn = await asyncio.sleep(1)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
# 异步关闭数据库连接
await asyncio.sleep(1)
async def func():
async with AsyncContextManager('xxx') as f:
result = await f.do_something()
print(result)
asyncio.run( func() )
4 uvloop
uvloop是asyncio的事件循环的替代方案。asyncio的事件循环的效率 < uvloop。
pip3 install uvloop
import asyncio
import uvloop
# 要使用uvloop替代asyncio内部的事件循环,只需要设置一下即可
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 其他异步操作不变,编写的asyncio代码和之前写的一样
# 内部的事件循环会自动变成uvloop
asyncio.run(xxx)
usgi:web server gateway interface(web服务网关接口)
asgi:async server gateway interface(异步服务网关接口)
wsgi、uwsgi、asgi了解:https://www.cnblogs.com/f-g-f/p/11294559.html
注意:一个非常块的asgi是:uvicorn
,fastapi框架中的asgi就是使用uvicorn,而uvicorn内部的事件循环是使用uvloop。
5 实战案例
5.1 异步redis
在使用python代码操作redis时,链接/操作/断开都是网络IO操作,所以可以用异步提高性能。
pip3 install aioredis
示例1:
import asyncio
import aioredis
async def execute(addr, passw=None):
print('开始执行', addr)
# 网络IO操作:创建redis连接
redis = await aioredis.create_redis(addr)
# 网络IO操作:在redis中设置哈希值car,内部再设置三个键值对,即:redis = {'car':{'key1':1, 'key2':2, 'key3':3 }}
await redis.hmset_dict('car', key1=1, key2=2, key3=3)
# 网络IO操作:去redis中获取值
result = await redis.hgetall('car', encoding='utf-8')
print(result)
redis.close()
# 网络IO操作:关闭redis连接
await redis.wait_closed()
print('结束', addr)
asyncio.run( execute('redis://localhost:6379') )
示例2:
import asyncio
import aioredis
async def execute(address, password=None):
print('开始执行', address)
# 网络IO操作:先去连接47.93.4.197:6379,遇到IO则自动切换任务去连接47.93.4.198:6379
redis = await aioredis.create_redis_pool(address, password=password)
# 网络IO操作:遇到IO会自动切换任务
await redis.hmset_dict('car', key1=1, key2=2, key3=3)
# 网络IO操作:遇到IO会自动切换任务
result = await redis.hgetall('car', encoding='utf-8')
print(result)
redis.close()
# 网络IO操作:遇到IO会自动切换任务
await redis.wait_closed()
task_list = [
execute('redis://47.93.4.197:6379', 'root!123'),
execute('redis://47.93.4.198:6379', 'root!123'),
]
asyncio.run(asyncio.wait(task_list))
5.2 异步mysql
pip install aiomysql
示例1:
import aiomysql
import asyncio
async def execute(host, password):
# 网络IO操作:连接mysql
conn = await aiomysql.connect(host=host, port=3306, user='root', password=password, db='db4')
# 网络IO操作:创建CURSOR
cur = await conn.cursor(aiomysql.cursors.DictCursor)
# 网络IO:执行SQL
await cur.execute('select * from book')
result = cur.fetchall()
print(result)
# 网络IO:关闭连接
await cur.close()
conn.close()
asyncio.run(execute('127.0.0.1','root'))
示例2:
import aiomysql
import asyncio
async def execute(host, password):
# 网络IO操作:连接mysql,先去连接192.168.1.10,遇到IO则自动切换去连接192.168.1.11
conn = await aiomysql.connect(host=host, port=3306, user='root', password=password, db='db4')
# 网络IO操作:创建CURSOR
cur = await conn.cursor(aiomysql.cursors.DictCursor)
# 网络IO:执行SQL
await cur.execute('select * from book')
result = cur.fetchall()
print(result)
# 网络IO:关闭连接
await cur.close()
conn.close()
tasks = [
execute('192.168.1.10', 'root'),
execute('192.168.1.11', 'root1')
]
asyncio.run(asyncio.wait(tasks))
5.3 FastAPI框架异步
需要安装FastAPI
pip3 install fastapi
pip3 install uvicorn (一个asgi,内部是基于uvloop)
示例:FastAPI.py
import asyncio
import aioredis
from aioredis import Redis
import fastapi
import uvicorn
app = FastAPI()
# 创建一个redis池
REDIS_POOL = aioredis.ConnectionsPool('redis://192.168.1.10:6379', password='root', minsize=1, maxsize=10)
@app.get('/')
def index():
'''普通操作接口'''
return {'message': 'Hello world'}
@app.get('/red')
async def red():
'''异步操作接口'''
print('请求来了')
await asyncio.sleep(3)
# 连接池获取一个连接
conn = await REDIS_POOL.acquire()
# 获取redis对象
redis = Redis(conn)
# 设置值
await redis.hmset_dict('car', key1=1, key2=2, key3=3)
# 读取值
result = await redis.hgetall('car', encoding='utf-8')
print(result)
# 连接归还连接池
REDIS_POOL.release(conn)
return result
if __name__ == '__main__':
uvicorn.run('FastAPI:app', host='127.0.0.1', port=5000, log_level='info')
# FastAPI是这个文件的名字
注意:这里是使用FastAPI框架来实现,同样的其他支持asgi(异步服务网关接口)的框架也可以用这种方式来实现,不支持异步的框架,可以使用3.6节中提到的线程池或进程池配合协程异步来实现。
5.4 爬虫异步
pip3 install aiohttp
import aiohttp
import asyncio
async def fetch(session, url):
print('发送请求:', url)
async with session.get(url, verify_ssl=False) as response:
text = await response.text()
print('得到结果:', url, len(text))
return text
async def main():
async with aiohttp.ClientSession() as session:
url_list = [
'https://python.org',
'https://www.baidu.com',
'https://www.pythonav.com'
]
task_list = [asyncio.create_task(fetch(session, url)) for url in url_list]
done, pending = await asyncio.wait(task_list)
if __name__ == '__main__':
asyncio.run(main())
总结
异步最大的意义:用最少的资源完成最多的事情。
通过一个线程利用其IO等待时间去做一个其他事情。
异步笔记总结:
- 1 协程实现方式可以通过第三方工具greenlet或者python自带的功能,例如:yield关键字、asyncio装饰器、async和await关键字
- 2 实现异步编程代码的流程
- 2.1 了解事件循环的存在意义,事件循环相当于一个用于监控任务的存储空间,可以将异步任务放入事件循环中被监控与被执行
- 2.2 因为现在我们使用的python版本都是3.7以上的了,所以经常会用async、await关键字去实现异步编程。
- 2.3 注意async with语句和async for语句都需要在异步函数中才能生效
- 3 了解其他框架支不支持异步实现,如果不支持的话则可以使用线程池或进程池来配合协程去实现异步,参考本文3.6章节
- 4 多使用异步来提高执行效率