随笔 - 148  文章 - 3  评论 - 2  阅读 - 11万

读后笔记 -- FastAPI 构建Python微服务 Chapter8:创建协程、事件和消息驱动的事务

返回目录

8.2 实现协程

1. FastAPI 框架使用协程的意义:

复制代码
FastAPI 使用协程(coroutines)有着重要的意义,尤其是在构建高性能的现代 Web 服务方面。协程是一种轻量级的并发机制,它们允许多个函数(或生成器对象)在一个单一的线程内并发运行。以下是使用协程的一些关键好处:

1. 高性能
  非阻塞性: 协程允许 FastAPI 处理 I/O 密集型任务而不会阻塞整个进程。这意味着 FastAPI 可以同时处理多个请求,而无需等待 I/O 操作完成。
  异步编程: 使用协程和异步编程模式,FastAPI 可以在等待 I/O 时执行其他任务,从而提高服务器的整体吞吐量和响应速度。
2. 资源效率
  内存占用低: 由于协程在单个线程内运行,它们使用的内存资源比多线程或多进程少得多。这对于处理大量并发连接特别有用。
  上下文切换开销小: 协程之间的上下文切换比线程之间的上下文切换要快得多,因此切换开销更小,提高了整体性能。
3. 易于编写和维护
  简洁的代码: 使用协程可以编写更简洁、易于理解和维护的代码。异步编程模式让代码更接近同步代码的写法,使得开发人员更容易理解和调试。
  避免回调地狱: 传统的异步编程通常依赖于回调函数,这可能导致难以管理的“回调地狱”。协程和 async/await 语法使得代码更易于组织和阅读。
4. 异步数据库访问
  数据库驱动: 许多现代数据库驱动程序支持异步操作,这使得 FastAPI 能够在等待数据库查询结果的同时处理其他请求。
  ORM 支持: 如 SQLAlchemy 等 ORM 工具提供了异步版本(如 asyncio 兼容的版本),使得 FastAPI 能够高效地与数据库交互。
5. 生态系统支持
  广泛的工具和库: FastAPI 社区提供了大量的异步兼容工具和库,包括但不限于数据库驱动、HTTP 客户端、消息队列客户端等。
  标准库支持: Python 3.7+ 的标准库也提供了对异步编程的支持,如 asyncio 模块,这使得 FastAPI 可以轻松地与其他异步组件集成。

总之,FastAPI 使用协程是为了提高应用程序的性能、资源利用率以及代码的可读性和可维护性。这使得 FastAPI 成为了构建高性能 Web 服务的理想选择。
复制代码

 

2. FastAPI 有两种实现协程的方式:

  • 应用 @asyncio.coroutine 装饰器 (仅适用于 Python 3.4 - 3.6)
  • 使用 async/awit 构造

 

3. async/await:

  • 1)该方法产生的协程为原生协程(native coroutine),不像生成器类型那样迭代;
  • 2)允许创建其他异步组件,如 async with 上下文管理器 和 async for 迭代器;

 

4. 实现协程的两个方式:

4.1 将单一和复杂的进程简化并分解为更小但更稳定可靠的协程,利用更多上下文的切换来提高应用程序的性能

复制代码
async def extract_profile(admin_details: Admin):
    profile = {}
    login = admin_details.login
    profile["firstname"] = admin_details.firstname
    profile["lastname"] = admin_details.lastname
    profile["age"] = admin_details.age
    profile["status"] = admin_details.status
    profile["birthday"] = admin_details.birthday
    profile["username"] = login.username
    profile["password"] = login.password
    await asyncio.sleep(1)
    return profile


async def extract_condensed(profiles):
    profile_info = " ".join([profiles["firstname"], profiles["lastname"], profiles["username"], profiles["password"]])
    await asyncio.sleep(1)
    return profile_info


async def decrypt_profile(profile_info):
    key = Fernet.generate_key()
    fernet = Fernet(key)
    encoded_profile = fernet.encrypt(profile_info.encode())
    return encoded_profile

async def extract_enc_admin_profile(admin_rec):
    """
    协程应用方式一: 将单一和复杂的进程简化并分解为更小但更稳定可靠的协程,利用更多上下文的切换来提高应用程序的性能
    实现: 链式模式,通过链来调用其他较小的协程
    :param admin_rec:
    :return:
    """
    p = await extract_profile(admin_rec)
    pinfo = await extract_condensed(p)
    encp = await decrypt_profile(pinfo)
    return encp
方式一:拆分成小的可靠的协程
复制代码

4.2 使用 asyncio.Queue 的管道 来实现协程切换

复制代码
import asyncio
from asyncio import Queue

from cryptography.fernet import Fernet

from models.data.nsms import Admin


async def process_billing(query_list):
    """
    协程应用方式二: 使用 asyncio.Queue 的管道 来实现协程切换
    队列结构中:一个/多个 生产者(将值放入队列) + 一个/多个 使用者(从队列中获取项目的任务)
    :param query_list:
    :return:
    """
    billing_list = []

    async def extract_billing(qlist, q: Queue):
        """Queue 的生产者"""
        assigned_billing = {}
        for record in qlist:
            await asyncio.sleep(2)
            assigned_billing["admin_name"] = "{} {}".format(record.firstname, record.lastname)
            assigned_billing["billing_items"] = record.login if not len(record.login) else None

            await q.put(assigned_billing)

    async def build_billing_sheet(q: Queue):
        """Queue 的消费者"""
        while True:
            await asyncio.sleep(2)
            assigned_billing = await q.get()
            name = assigned_billing["admin_name"]
            billing_items = assigned_billing["billing_items"]
            if not billing_items:
                for item in billing_items:
                    billing_list.append({"admin_name": name, "billing": item})
            else:
                billing_list.append({"admin_name": name, "billing": None})

            q.task_done()

    # 创建了一个异步队列 q,用于在不同的异步任务之间传递数据
    q = asyncio.Queue()
    # 创建了一个名为 build_sheet 的异步任务,该任务执行 build_billing_sheet(q) 函数。该函数负责从队列 q 中取出数据并进行处理
    # asyncio.create_task() 会立即启动这个任务,并允许它与其他任务并发运行
    build_sheet = asyncio.create_task(build_billing_sheet(q))
    # 创建了另一个异步任务 extract_billing(query_list, q),该任务负责从 query_list 中提取数据,并将这些数据放入队列 q 中
    # asyncio.gather() 用来等待所有传入的任务完成。这里只传入了一个任务,但你可以向 asyncio.gather() 传入多个任务,它会等待所有任务都完成
    await asyncio.gather(asyncio.create_task(extract_billing(query_list, q)))
    # q.join() 是一个阻塞调用,它会等待直到队列中的所有任务都被消费完并且标记为完成
    # 确保通过 extract_building() 传递到管道的所有项目都由 build_billing_sheet() 获取和处理
    await q.join()
    # 在确认所有数据都已经处理完毕后,取消 build_sheet 任务。如果 build_billing_sheet 仍在运行,这将尝试停止它。
    build_sheet.cancel()
    return billing_list
方式二: 使用 asyncio.Queue 的管道 来实现协程切换
复制代码

 


8.3 创建异步后台任务

使用 async/await 结构创建和执行异步后台进程 + BackgroundTasks 注入 API 服务作为后台任务

复制代码
from fastapi import APIRouter, BackgroundTasks

from datetime import date

from starlette.responses import JSONResponse

from config.db.sqlalchemy_async_connect import AsyncSessionFactory
from repository.billing import BillingVendorRepository
from services.billing import generate_billing_sheet, create_total_payables_year, create_total_payables_year_celery

router = APIRouter()


@router.post("/billing/save/csv")
async def save_vendor_billing(billing_date: date, tasks: BackgroundTasks):
    async with AsyncSessionFactory() as sess:
        async with sess.begin():
            """创建多任务的异步后台任务"""
            repo = BillingVendorRepository(sess)
            result = await repo.join_vendor_billing()
            tasks.add_task(generate_billing_sheet, billing_date, result)
            tasks.add_task(create_total_payables_year, billing_date, result)
            return {"message": "done"}
api 接口代码(api.billing.py)
复制代码
复制代码
import asyncio
import os


async def generate_billing_sheet(billing_date, query_list):
    file_path = os.getcwd() + "/data/billing-" + str(billing_date) + ".csv"
    with open(file_path, mode = "a") as sheet:
        for vendor in query_list:
            billing = vendor.billings
            for record in billing:
                if billing_date == record.date_billed:
                    entry = ";".join([str(record.date_billed), vendor.account_name, vendor.account_number, str(record.payable), str(record.total_issues)])
                    sheet.write(entry)
                await asyncio.sleep(1)


async def create_total_payables_year(billing_date, query_list):
    total = 0.0
    for vendor in query_list:
        billing = vendor.billings  # 使用 SQLAlchemy 的关系属性
        for record in billing:
            if billing_date == record.date_billed:
                total += record.payable
                await asyncio.sleep(1)

    print(total)
services 代码(services.billing.py)
复制代码

 

posted on   bruce_he  阅读(216)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2020-07-27 Imooc 上课总结(Java)-- Idea Java 使用心得 + 快捷键
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示

目录导航