python协程方法及其示例

302 阅读12分钟

协程

  1. 协程不是计算机提供的,而是人为创造的
  2. 协程是一种用户态的上下文切换技术,是一种异步IO

一、异步IO是什么

假设你要去餐厅吃饭,你有两种方式:

  • 同步IO:你自己去点菜,然后在柜台傻傻等待菜做好,拿到菜后再去找座位。这样的缺点是你在等待菜的过程中不能做其他事情,而且可能会占用柜台的空间。
  • 异步IO:你先找到座位,然后用手机扫码点菜,然后做其他事情(比如看书、聊天等),当收到通知菜做好了时再去柜台取菜。这样的优点是你可以利用等待时间做其他事情,而且不会占用柜台的空间。

二、协程是什么?

协程是一种特殊的函数,它可以在某个地方挂起,并且可以重新在挂起处继续运行。协程之间的切换是由程序控制的,不需要操作系统的干预。

协程相比于线程,有以下几个优点:

  • 协程更加轻量级,创建和销毁的开销更小
  • 协程可以避免多线程的锁和同步问题
  • 协程可以提高IO操作的效率,减少等待时间

协程并发.png

线程切换和协程切换

虚假的多线程技术

(例子选自知乎:事件循环和协程:从生成器到协程 - 知乎 (zhihu.com)) 一个cpu核心状态下,多线程技术其实是假的。假的是什么意思?就是根本没有多线程这种玩意儿,那系统是怎么实现两段代码同时运行的呢?

答案:骗你

例如:小明推两个箱子,因为他只有一个人,所以他先推左边的箱子,然后再推右边的箱子。在平常人看来,就是左边的箱子先移动,右边的箱子后移动。这时候,小明获得了闪电侠⚡️的能力,他左右来回移动接近光速,那你看到的两个箱子就是相当于同时向前移动了

上述切换是在内核态实现的,即一个核心下快速进行切换。代码运行结果不可预计线程的切换有损耗

如果只有一个线程下阻塞了,能不能线程内如何快速切换呢?

答案:协程,协程是一种编程模式,可以让一个函数在执行过程中暂停并切换到另一个函数继续执行,然后再回到原来的函数继续执行。协程的优点是可以减少系统内核级的线程切换开销,提高CPU的利用率和并发能力

为什么用协程?

例子: 假设你需要煮饭和洗碗。

如果你用单线程的方式,你就要先把饭煮好,然后再去洗碗;

如果你用多线程的方式,你就要开两个火眼,同时煮饭和洗碗,但是这样会消耗更多的资源;

如果你用协程的方式,你就可以在煮饭的时候等待水开,然后切换到洗碗的任务,在洗完碗后再切换回煮饭的任务。这样既节省了资源又提高了效率。

协程的缺点有什么?

  1. 分离状态:因为协程不能存储状态,所以开发者需要自己设计办法将状态分离出来。
  1. 调试困难:由于协程本质上是一种比较灵活、异步的处理方式,因此调试起来也比较困难。
  2. 内存消耗:由于协程的创建会额外消耗内存,所以在使用大量协程的情况下,会消耗大量的内存。
  3. 切换开销:当协程切换时,会消耗一定的时间,从而影响程序的性能。

协程使用场景有哪些?

  1. 实现异步I/O,比如socket,文件I/O
  1. 消息队列的消费者,比如RabbitMQ,Kafka
  1. 分布式任务调度,比如Celery
  1. 并发控制,比如爬虫,定时任务,任务流水线

三、协程如何实现?

  1. greenlet等早期模块
  2. yield关键字(py3.11已经无法通过该关键字实现协程,仅能用于同步/异步迭代器)
  3. asyncio装饰器(py3.4)【推荐】(asynchronization的缩写)
  4. async、await关键字(py3.5)【推荐】

四、协程基本概念

(一)事件循环

官话:事件循环通过注册事件和事件处理函数,监听特定的事件发生,并调用相应的处理函数。

人话:多线程之间的调度由系统处理,协程之间的调度由程序员自己设置,设置这个调度的方式,便是事件循环

(二)协程函数及对象

  1. 协程函数: async def 函数名
  2. 协程对象: 执行 协程函数() 得到的协程对象
 # 例一
 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())

(八)异步迭代器

  1. 什么是异步迭代器?
  • 通过 aiter() 方法返回一个可等待(awaitable)对象。
  • 然后通过 anext() 方法返回每个元素或者引发一个 StopAsyncIteration 异常来结束迭代。
  1. 怎么定义异步迭代器
  • 函数的实现:使用async def 定义一个异步生成器函数,然后用yield语句返回每个元素
  • 类的实现:实现 aiter()anext() 方法,分别返回自身和每个元素或者StopAsyncIteration异常。
  1. 怎么遍历异步迭代器
  • 使用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 — uvloop Documentation

image-20230225145529079.png

 # 要使用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

扩展知识:

  1. Uvicorn是一个用Python实现的ASGI(异步服务器网关接口)web服务器,它支持HTTP/1.1和WebSockets,可以运行FastAPI等异步框架的应用程序。
  2. Uvicorn可以提供高性能、低延迟和高并发的服务。可以直接使用Uvicorn来开发和测试你的应用程序,但是如果你想在生产环境中部署你的应用程序,你可能需要使用Gunicorn来管理Uvicorn的多个工作进程。这样可以让你更灵活地调整工作进程的数量、重启工作进程或者升级服务器。
  3. ASGI是异步服务器网关接口,它是WSGI的扩展版本,旨在为Python Web服务、框架和应用之间提供一个标准的异步接。它能够处理多种通用的协议类型,包括HTTP,HTTP2和WebSocket。相较于WSGI定义了同步的Python应用间的通信规范,ASGI同时囊括了同步和异步应用的通信规范,并且向后兼容遵循WSGI的应用、服务以及框架。ASGI的作用是为了支持更高效和更灵活的Python Web开发,特别是在面对高并发、长连接、实时推送等场景时。
  4. 性能基准测试结果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())