python异步

fastapi 是一个异步的web框架。

Starlette 是一个轻量级、快速的 Python ASGI 框架,专为构建高性能异步 Web 应用和微服务而设计。它是 FastAPI 的核心依赖之一,许多 FastAPI 的功能都基于 Starlette 提供的组件。Starlette 以其简洁的设计和丰富的功能而著称,非常适合构建现代异步 Web 应用。

uvicorn 是一个快速、轻量级的 Python ASGI(Asynchronous Server Gateway Interface)服务器,专为运行高性能的异步 Web 应用和框架而设计。uvicorn 是使用 uvloop 和 httptools 构建的,这使它在处理 HTTP 请求时表现出色,尤其适用于基于异步 I/O 的应用。

uvloop 是一个用于 Python 的高性能事件循环,它是 asyncio 的一个替代实现。uvloop 是基于 libuv 构建的,libuv 是一个跨平台的异步 I/O 库,广泛用于 Node.js 和其他异步系统中。

fastapi的ASGI服务器是可选的,一般会选择uvicorn。
uvicorn的事件循环也是可选的,一般默认使用asyncio默认的事件循环,也可以设置为uvloop。

事件循环:

  • 单线程:在单个线程中只能有一个活动的事件循环。
  • 多线程:你可以在不同的线程中创建和运行各自的事件循环,但每个线程只能有一个事件循环在运行。

asyncio.get_event_loop():
用于获取当前线程的事件循环。如果不存在,则创建并返回一个新的。
适合主线程中的异步任务启动或管理。

asyncio.get_running_loop():
用于获取当前正在运行的事件循环。如果没有运行的事件循环则抛出错误。
适合在已经运行的协程或回调函数中使用。

asyncio.new_event_loop():
总是创建并返回一个新的事件循环实例,不会自动设置为当前线程的事件循环。
适合需要手动管理事件循环的场景,特别是在多线程或测试环境中。

asyncio.to_thread():
用于在一个单独的线程执行一个同步函数,从而不阻塞当前线程的事件循环。
即可以直接在前面加 await执行,又可以生成异步task,并使用asyncio.gather()来与其他异步任务并发执行。
asyncio.to_thread的工作机制
​​不会为函数创建新的事件循环​​,它的工作原理完全不同:

​​线程池执行​​:
将同步函数提交到ThreadPoolExecutor线程池
在单独的线程中运行同步函数
完全绕过事件循环机制
​​通信桥梁​​:

async def to_thread_flow():
    # 主线程(有事件循环)
    │
    │ asyncio.to_thread(func)
    ↓
    # 子线程(无线程循环) → 执行func()
    │
    ↑
    # 结果通过Future传回主线程
    │
    await获取结果

​​与事件循环的关系​​:
主线程的事件循环只负责监视Future状态
函数实际执行在无事件循环的纯线程环境
通过线程间通信将结果传回事件循环线程

windows系统事件循环策略: asyncio模块使用的windows系统的默认的事件循环策略。
asyncio.WindowsSelectorEventLoopPolicy: Python 3.7 及之前的版本
asyncio.WindowsProactorEventLoopPolicy: Python 3.8 及之后的版本

uvloop.EventLoopPolicy(): uvloop自己的事件循环策略

有时候,可能是一些库WindowsProactorEventLoopPolicy支持的不好,在使用asyncio.run()运行异步函数时经常报event loop is closed这样的错误,这时可以考虑改变事件循环策略。
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
或者使用旧的方式运行异步函数:

loop = asyncio.get_event_loop()
loop.run_until_complete(fun())

事件循环的创建:
asyncio.run()函数会自动创建一个事件循环
像web框架,比如fastapi,会在服务器启动时自动创建一个事件循环。
uvicorn.run(f"{app_model_name}:app", host='0.0.0.0', reload=False)
在单线程中,可以使用asyncio.get_event_loop()来创建。
在多线程中,一般在子线程中使用asyncio.new_event_loop()来创建子线程自已的事件循环。
当你明确知道代码是在事件循环中运行时,可以用asyncio.get_running_loop()方法获取当前运行的事件循环, 一般用于异步函数内部。

下面是一个在子线程中使用事件循环的示例:

import asyncio
import threading

async def hello():
    print(f"Hello from thread {threading.current_thread().name}")
    await asyncio.sleep(1)
    print(f"Goodbye from thread {threading.current_thread().name}")

def start_loop():
    loop = asyncio.new_event_loop()  # 创建一个新的事件循环
    asyncio.set_event_loop(loop)     # 设置为当前线程的事件循环
    loop.run_until_complete(hello())

# 在主线程中运行一个事件循环
asyncio.run(hello())

# 在一个子线程中运行另一个事件循环
thread = threading.Thread(target=start_loop)
thread.start()
thread.join()

Coroutine: 直接运行协程函数行到Coroutine
Task: 使用asyncio.create_task(Coroutine)创建Task

在事件循环中真正被执行的是Task

await asyncio.gather() 是等待所有任务执行完毕。 参数即可以是协程,也可以是任务。如果传递进来的是协程,gather函数内部会将其先转化为任务。

asyncio.create_task(Coroutine) 创建好的任务会立即被添加到事件循环中。

下面两种调用方法基本等价。 第一种是一创建任务就被立即加入到事件循环中了。 第二种是在调用gather函数的时候coroutine才被包装成任务并加到事件循环中。除非代码中在gather之前有些delay的代码,否则性能基本一致。

import asyncio

async def my_coroutine(name, delay):
    print(f"{name} started")
    await asyncio.sleep(delay)
    print(f"{name} finished")

async def main_coroutine():
    task1 = asyncio.create_task(my_coroutine("Task1", 1))  # 显式创建任务
    task2 = asyncio.create_task(my_coroutine("Task2", 2))
    await asyncio.gather(task1, task2)

async def main_task():
    await asyncio.gather(my_coroutine("Task1", 1), my_coroutine("Task2", 2))

# asyncio.run(my_coroutine())  # 输出可能包含任务在 gather 前启动的日志
# asyncio.run(main_coroutine())  # 输出显示任务在 gather 后启动

将协程转化成任务的优势
并发执行多个任务。 即便asyncio.gather可以传递协程做为参数,但其实还是将其隐式转换成了任务。 asyncio.wait函数也需要任务做为参数。
任务可以被取消

async def main_cancel():
    task = asyncio.create_task(fetch_data("请求", 10))
    await asyncio.sleep(1)
    task.cancel()  # 取消任务
    try:
        await task
    except asyncio.CancelledError:
        print("任务已取消")

可以检查任务状态

if not task.done():
    print("任务仍在运行")

可以对任务设置超时时间:

try:
    await asyncio.wait_for(task, timeout=5)
except asyncio.TimeoutError:
    print("任务超时")

可为任务绑定完成后的回调函数:

def callback(future):
    print(f"任务结果: {future.result()}")

task.add_done_callback(callback)

通过 task.result() 直接获取协程返回值(需任务完成):

result = await task
# 或
result = task.result()

任务异常不会立即崩溃程序,可通过 task.exception() 获取:

try:
    await task
except Exception as e:
    print(f"任务异常: {e}")

# 或
if task.exception():
    print(task.exception())

通过 asyncio.Semaphore 限制最大并发任务数,防止资源耗尽:

sem = asyncio.Semaphore(5)

async def limited_task():
    async with sem:
        await fetch_data("请求", 1)

tasks = [limited_task() for _ in range(100)]
await asyncio.gather(*tasks)

即使主协程不等待,任务仍可在后台运行:

async def main_background():
    asyncio.create_task(fetch_data("后台任务", 5))
    print("主协程继续执行")  # 不等待后台任务完成
posted @ 2024-08-20 17:02  RolandHe  阅读(153)  评论(0)    收藏  举报