协程
- 协程不是计算机提供的,而是人为创造的
- 协程是一种用户态的上下文切换技术,是一种异步IO
一、异步IO是什么
假设你要去餐厅吃饭,你有两种方式:
- 同步IO:你自己去点菜,然后在柜台傻傻等待菜做好,拿到菜后再去找座位。这样的缺点是你在等待菜的过程中不能做其他事情,而且可能会占用柜台的空间。
- 异步IO:你先找到座位,然后用手机扫码点菜,然后做其他事情(比如看书、聊天等),当收到通知菜做好了时再去柜台取菜。这样的优点是你可以利用等待时间做其他事情,而且不会占用柜台的空间。
二、协程是什么?
协程是一种特殊的函数,它可以在某个地方挂起,并且可以重新在挂起处继续运行。协程之间的切换是由程序控制的,不需要操作系统的干预。
协程相比于线程,有以下几个优点:
- 协程更加轻量级,创建和销毁的开销更小
- 协程可以避免多线程的锁和同步问题
- 协程可以提高IO操作的效率,减少等待时间
线程切换和协程切换
虚假的多线程技术
(例子选自知乎:事件循环和协程:从生成器到协程 - 知乎 (zhihu.com)) 一个cpu核心状态下,多线程技术其实是假的。假的是什么意思?就是根本没有多线程这种玩意儿,那系统是怎么实现两段代码同时运行的呢?
答案:骗你。
例如:小明推两个箱子,因为他只有一个人,所以他先推左边的箱子,然后再推右边的箱子。在平常人看来,就是左边的箱子先移动,右边的箱子后移动。这时候,小明获得了闪电侠⚡️的能力,他左右来回移动接近光速,那你看到的两个箱子就是相当于同时向前移动了。
上述切换是在内核态实现的,即一个核心下快速进行切换。代码运行结果不可预计且线程的切换有损耗
如果只有一个线程下阻塞了,能不能线程内如何快速切换呢?
答案:协程,协程是一种编程模式,可以让一个函数在执行过程中暂停并切换到另一个函数继续执行,然后再回到原来的函数继续执行。协程的优点是可以减少系统内核级的线程切换开销,提高CPU的利用率和并发能力
为什么用协程?
例子: 假设你需要煮饭和洗碗。
如果你用单线程的方式,你就要先把饭煮好,然后再去洗碗;
如果你用多线程的方式,你就要开两个火眼,同时煮饭和洗碗,但是这样会消耗更多的资源;
如果你用协程的方式,你就可以在煮饭的时候等待水开,然后切换到洗碗的任务,在洗完碗后再切换回煮饭的任务。这样既节省了资源又提高了效率。
协程的缺点有什么?
- 分离状态:因为协程不能存储状态,所以开发者需要自己设计办法将状态分离出来。
- 调试困难:由于协程本质上是一种比较灵活、异步的处理方式,因此调试起来也比较困难。
- 内存消耗:由于协程的创建会额外消耗内存,所以在使用大量协程的情况下,会消耗大量的内存。
- 切换开销:当协程切换时,会消耗一定的时间,从而影响程序的性能。
协程使用场景有哪些?
- 实现异步I/O,比如socket,文件I/O
- 消息队列的消费者,比如RabbitMQ,Kafka
- 分布式任务调度,比如Celery
- 并发控制,比如爬虫,定时任务,任务流水线
三、协程如何实现?
- greenlet等早期模块
- yield关键字(py3.11已经无法通过该关键字实现协程,仅能用于同步/异步迭代器)
- asyncio装饰器(py3.4)【推荐】(asynchronization的缩写)
- async、await关键字(py3.5)【推荐】
四、协程基本概念
(一)事件循环
官话:事件循环通过注册事件和事件处理函数,监听特定的事件发生,并调用相应的处理函数。
人话:多线程之间的调度由系统处理,协程之间的调度由程序员自己设置,设置这个调度的方式,便是事件循环
(二)协程函数及对象
- 协程函数: async def 函数名
- 协程对象: 执行 协程函数() 得到的协程对象
# 例一
async def func():
pass
# 得到协程对象, 内部代码不会执行
result = func()
# 交给事件循环器运行起来
asyncio.run(result)
# 例二
import asyncio
async def func1():
print (1)
# 网络10请求:下载一张图片
await asyncio.sleep(2) # 遇到I0耗时操作,自动化切换到tasks中的其他任务print(2)
async def func2():
print (3)
# 网络IO请求:下载一张图片
await asyncio.sleep(2) # 遇到I0耗时操作,自动化切换到tasks中的其他任务print(4)
tasks = [ asyncio.ensure_future( func1() ), asyncio.ensure_future( func2() ) ]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
# 例三
import asyncio
async def say_after(delay, what):
# 3. 当执行到asyncio.sleep(delay)时,main()函数会暂停,并将控制权交给事件循环。
# 4. 事件循环会调度其他可运行的任务(如果有),或者等待一些I/O操作完成(如果有)。
await asyncio.sleep(delay)
# 5. 当等待了1秒后,事件循环会恢复say_after(1, 'hello')协程的执行,并打印出“hello”。
print(what)
# 6. return一个None
async def main():
print(f"started at {time.strftime('%X')}")
# 2. main()函数会创建两个say_after()协程对象,并分别传入不同的参数
await say_after(1, 'hello')
# 7. 继续执行到await say_after(2, 'world')
# 8. 同样地,事件循环会等待2秒后,恢复say_after(2, 'world')协程的执行,并打印出“world”。
await say_after(2, 'world')
# 9. 最后,事件循环会结束main()函数的执行,并关闭自己。
print(f"finished at {time.strftime('%X')}")
# 1. 当调用asyncio.run(main())时,会创建一个事件循环,并运行main()函数作为入口点。
asyncio.run(main())
- asyncio.run()是一个方便的函数,它可以自动创建和关闭一个事件循环,并运行一个协程作为入口点。它是Python 3.7新加的接口,用于简化异步编程的启动过程。
- loop = asyncio.get_event_loop() 是一种更低层次的方式,它需要手动获取和关闭事件循环,并运行一个或多个协程任务直到完成。它可以用于Python 3.4及以上版本,但需要更多的代码和控制。
一般来说,如果你只需要运行一个简单的异步程序,你可以使用asyncio.run()来快速启动。如果你需要运行一个复杂的异步程序,或者需要对事件循环进行更细粒度的控制,你可以使用loop = asyncio.get_event_loop() 来自定义启动。
(四)await关键字
await是一个只能在协程函数中使用的关键词,用于在遇到IO操作时悬挂当前协程,等待异步调用的结果。
await后面必须跟一个可等待对象(awaitable object),可等待对象有一下几种
- 协程对象:async定义的函数或者生成器
- 任务Task:对协程对象的封装
- Future:表示异步操作结果的对象
# 例四
import asyncio
async def fetch_data():
print("开始获取fetch_data数据")
await asyncio.sleep(1) # 模拟IO操作
print("fetch_data数据获取完毕")
return {"data": 1}
async def print_numbers():
for i in range(5):
print(i)
await asyncio.sleep(0.25)
async def main():
task1 = asyncio.create_task(fetch_data()) # 创建一个任务1
task2 = asyncio.create_task(print_numbers()) # 创建另一个任务2
value = await task1 # 等待任务1返回结果
print(f"任务1:{value}") # 打印结果
await task2 # 等待任务2完成
if __name__ == "__main__":
asyncio.run(main()) # 运行主函数
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# 输出内容如下
开始获取fetch_data数据 # 任务1 进入IO等待
0 # 执行任务2
1
2
3
fetch_data数据获取完毕 # 任务1 IO等待结束
任务1:{'data': 1} # 获取任务1 的结果
4 # 继续执行任务2
(五)Task对象
一种与事件循环进行交互的方式,它把协程封装成一个实例,并追踪协程的运行和完成状态,用于未来获取协程的结果。
核心作用:是在事件循环中添加多个并发任务,并进行管理和调度
创建方式:Task对象可以使用 asyncio.create_task()函数或 asyncio.ensure_future() 函数来创建。
- 如何创建和运行一个简单的Task对象:
import asyncio
async def say_hello():
print("Hello")
await asyncio.sleep(1)
print("World")
async def main():
# 创建一个Task对象
task = asyncio.create_task(say_hello())
# 等待Task对象完成
await task
# 获取事件循环
loop = asyncio.get_event_loop()
# 运行主函数
loop.run_until_complete(main())
- 如何使用wait()函数等待多个Task对象:
import asyncio
async def work(x):
for _ in range(3):
print("Work {} is running".format(x))
await asyncio.sleep(1)
async def main():
# 创建三个Task对象
task1 = asyncio.create_task(work(1), name="1")
task2 = asyncio.create_task(work(2), name="2")
task3 = asyncio.create_task(work(3), name="3")
# 等待所有Task对象完成,设置超时时间为5秒
done, pending = await asyncio.wait([task1, task2, task3], timeout=5)
# done和pending是两个集合,分别包含已完成和未完成的Task对象。
# 可以使用for循环或列表推导式来遍历这些集合,并获取每个Task对象的结果或异常。
# 打印所有的结果
for task in done:
print(task.result())
# 获取事件循环
loop = asyncio.get_event_loop()
# 运行主函数
loop.run_until_complete(main())
- 如何取消或停止一个正在运行的Task对象:
import asyncio
async def work(x):
for _ in range(10):
print("Work {} is running".format(x))
await asyncio.sleep(1)
async def main():
# 创建一个Task对象
task = loop.create_task(work(1))
# 等待一段时间后取消任务执行
await asyncio.sleep(5)
# 取消任务执行,会抛出CancelledError异常,可以捕获并处理该异常。
try:
task.cancel()
await task
except Exception as e:
print(e)
# 获取事件循环
loop = asyncio.get_event_loop()
# 运行主函数
loop.run_until_complete(main())
(六)Future对象
Python标准库中有两个名为Future的类:concurrent.futures.Future 和 asyncio.Future。它们的作用相同,但是不兼容,不能混用。
concurrent.futures.Future: 线程池、进程池实现异步操作时用到的对象
asyncio.Future:协程异步操作时用到的对象
Future对象是一种表示已经完成或者尚未完成的延迟计算的对象。它有以下特点:
- 它可以保存一个结果或一个异常。
- 它可以被取消或完成。
- 它可以添加回调函数,在完成时运行该回调函数。
- 它通常不应该由用户创建,而是由并发框架生成。
(七)协程 + 线程/进程的混合异步编程
import time
import asyncio
import concurrent.futures
def func1():
# 某个耗时操作
time.sleep(2)
return "DONE"
async def main():
loop = asyncio.get_running_loop()
# 1. Run in the default loop's executor ( 默认ThreadPoolExecutor )
# 第一步:内部会先调用 ThreadPoolExecutor 的 submit 方法去线程池中申请一个线程去执行func1函数,并返回一个concurrent.futures.Future对象
# 第二步:调用asyncio.wrap_future将concurrent.futures.Future对象包装为asycio.Future对象。
# 因为concurrent.futures.Future对象不支持await语法,所以需要包装为 asycio.Future对象 才能使用。
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)
if __name__ == "__main__":
asyncio.run(main())
- 例子: 多线程读取多个txt文件,然后使用协程判断key是否在数据仓库中,并将空记录写入日志文件
import threading
import time
import asyncio
from concurrent.futures import ThreadPoolExecutor
def read_file(file_name):
with open(file_name) as f:
for line in f:
yield line.strip()
async def check_key(key):
# 模拟判断key是否在数据仓库中的异步操作
await asyncio.sleep(0.01)
return key.endswith('0')
async def write_log(key):
# 模拟将空记录写入日志文件的异步操作
await asyncio.sleep(0.01)
print(f'write {key} to log file')
def process_file(file_name):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = []
# 使用生成器循环遍历
for key in read_file(file_name):
task = loop.create_task(check_key(key))
# 添加回调函数
task.add_done_callback(lambda t: write_log(t.result()) if not t.result() else None)
# 添加到任务队列内
tasks.append(task)
# 执行事件循环
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
if __name__ == '__main__':
start_time = time.time()
executor = ThreadPoolExecutor(max_workers=3)
files = ['file1.txt', 'file2.txt', 'file3.txt']
for file in files:
executor.submit(process_file, file)
executor.shutdown(wait=True)
end_time = time.time()
print(f'total time: {end_time - start_time}')
- 例子: 使用事件循环的run_in_executor()方法,在一个线程中运行协程,同时执行计算斐波那契数列和请求延迟网站的任务。代码如下:
import requests
import time
from concurrent.futures import ThreadPoolExecutor
def calc_fib(n):
""" 计算斐波那契数列 """
if n <= 2:
return 1
return calc_fib(n - 1) + calc_fib(n - 2)
async def get_url(url):
""" 获取网站访问请求 """
response = requests.get(url)
return response.text
async def main():
start_time = time.time()
# 启动事件循环
loop = asyncio.get_running_loop()
# 初始化线程池
executor = ThreadPoolExecutor(max_workers=4)
# 利用线程池创建协程对象
fib_task = loop.run_in_executor(executor, calc_fib, 36)
# 创建Task任务
url_task1 = asyncio.create_task(get_url('https://httpbin.org/delay/5'))
url_task2 = asyncio.create_task(get_url('https://httpbin.org/delay/5'))
# 协程并行执行
fib_result, url_result1, url_result2 = await asyncio.gather(fib_task, url_task1, url_task2)
end_time = time.time()
print(f'fib result: {fib_result}')
print(f'url result length: {len(url_result1)}')
print(f'url result length: {len(url_result2)}')
print(f'total time: {end_time - start_time}')
if __name__ == '__main__':
asyncio.run(main())
(八)异步迭代器
- 什么是异步迭代器?
- 通过 aiter() 方法返回一个可等待(awaitable)对象。
- 然后通过 anext() 方法返回每个元素或者引发一个 StopAsyncIteration 异常来结束迭代。
- 怎么定义异步迭代器
- 函数的实现:使用async def 定义一个异步生成器函数,然后用yield语句返回每个元素
- 类的实现:实现 aiter() 和 anext() 方法,分别返回自身和每个元素或者StopAsyncIteration异常。
- 怎么遍历异步迭代器
- 使用async for语句来遍历异步迭代器, async_for会处理异步迭代器的 anext() 方法所返回的可等待对象,直到其引发一个 StopAsyncIteration 异常
# 异步生成器函数
async def async_generator():
for i in range(10):
await asyncio.sleep(1) # 模拟异步IO操作
yield i # 返回每个元素
# 使用async for循环
async def main():
async for i in async_generator():
print(i)
# 运行协程
asyncio.run(main())
# 定义一个类,实现__aiter__()和__anext__()方法
class AsyncCounter:
def __init__(self, stop):
self.current = 0
self.stop = stop
def __aiter__(self):
# 返回一个可等待(awaitable)对象
return self
async def __anext__(self):
# 向下取值
if self.current < self.stop:
await asyncio.sleep(1) # 模拟异步IO操作
r = self.current # 返回每个元素
self.current += 1
return r
else:
raise StopAsyncIteration # 抛出异常
async def main():
# 使用async for循环, 使用async with语句, 必须嵌套到协程函数里面去
async for i in AsyncCounter(10):
print(i)
# 运行协程
asyncio.run(main())
(九)异步的上下文管理器
异步上下文管理器是指能够在__enter__和__exit__方法处暂停执行的上下文管理器。即python的With用法
为了实现这样的功能,需要定义两个新的方法: aenter__和__aexit 。这两个方法都必须返回一个awaitable对象。
# 定义一个异步上下文管理器
class AsyncFile:
def __init__(self, filename):
self.filename = filename
async def __aenter__(self):
self.file = await aiofiles.open(self.filename) # 异步打开文件
return self.file # 返回文件对象
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.file.close() # 异步关闭文件
async def main():
# 使用async with语句, 必须嵌套到协程函数里面去
async with AsyncFile("test.txt") as f: # 调用异步上下文管理器
content = await f.read() # 异步读取文件内容
print(content)
# 运行协程
asyncio.run(main())
(十)高性能协程库 uvloop
python uvloop是一个快速的,替代asyncio内置的事件循环的python库uvloop是用Cython写的,建于libuv之上,可以使asyncio更快,甚至比nodejs和gevent等其他框架快2-4倍
# 要使用uvloop,你只需要安装它,并在你的代码中设置它为默认的事件循环
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
注意:Asgi -> uvicorn内部使用了uvloop
五、实战案例
(一)异步Redis
python代码操作redis(链接、操作、断开)都是网络IO
pip3 install aioredis
import asyncio
import asyncio_redis
async def example():
# Create connection
connection = await asyncio_redis.Connection.create(host='localhost', port=6379)
# Set a key
await connection.set('my_key', 'my_value')
# Get a key
result = redis.hgetall('my_key', encoding='utf-8')
print(result)
# When finished, close the connection.
connection.close()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(example())
(二)异步mysql
pip3 install aiomysql
import asyncio
import aiomysql
async def test_example(loop):
# 网络IO:连接mysql
pool = await aiomysql.create_pool(host='127.0.0.1', port=3306,
user='root', password='',
db='mysql', loop=loop)
# 异步上下文管理器
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT 42;")
print(cur.description)
# 取值
(r,) = await cur.fetchone()
assert r == 42
# 网络IO关闭数据库
pool.close()
await pool.wait_closed()
loop = asyncio.get_event_loop()
loop.run_until_complete(test_example(loop))
(三)FastAPI框架
pip3 install fastapi
pip3 install uvicorn
扩展知识:
- Uvicorn是一个用Python实现的ASGI(异步服务器网关接口)web服务器,它支持HTTP/1.1和WebSockets,可以运行FastAPI等异步框架的应用程序。
- Uvicorn可以提供高性能、低延迟和高并发的服务。可以直接使用Uvicorn来开发和测试你的应用程序,但是如果你想在生产环境中部署你的应用程序,你可能需要使用Gunicorn来管理Uvicorn的多个工作进程。这样可以让你更灵活地调整工作进程的数量、重启工作进程或者升级服务器。
- ASGI是异步服务器网关接口,它是WSGI的扩展版本,旨在为Python Web服务、框架和应用之间提供一个标准的异步接。它能够处理多种通用的协议类型,包括HTTP,HTTP2和WebSocket。相较于WSGI定义了同步的Python应用间的通信规范,ASGI同时囊括了同步和异步应用的通信规范,并且向后兼容遵循WSGI的应用、服务以及框架。ASGI的作用是为了支持更高效和更灵活的Python Web开发,特别是在面对高并发、长连接、实时推送等场景时。
- 性能基准测试结果FastAPI vs. Express.js vs. Flask vs. Nest.js Benchmark - Travis Luong
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def hello():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
def get_item(item_id: int):
#这个请求会阻塞其他请求。
return {"item_id": item_id}
@app.get("/async")
async def async_hello():
#这个请求不会阻塞其他请求。
await asyncio.sleep(3) # 模拟耗时操作
return {"message": "Hello from async"}
uvicorn main:app --reload
(四)aiohttp
pip install aiohttp
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch(session, 'https://www.baidu.com')
print(html)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())