自学内容网 自学内容网

异步框架FastAPI基础

FastAPI框架概述

随着计算机行业的发展,CPU能力越来越强,为了给用户带来更好的体验,对系统单位时间内的处理能力要求也越来越高,在Python中早期的并发模式实现主要源于多线程或者多进程,然而Python多线程并没有把多核CPU的 优势发挥的很好,它只是表面上的一致并发,本质上还是通过线程调度来出让GIL(全局解释器锁),从而达到了并发的效果,然而这还是一种“单核单线程模式”,对于多核CPU,Python多线程无法分不到多核CPU上执行,所以说在某种程度上并没有发挥多核CPU的作用

随着Python版本的迭代,引入了协程的概念,随之而生的asyncio异步应用也逐渐成熟且应用越来越广泛,FastAPI对于异步的支持也让他有别于Flask和Django等同步框架

GIL是Global Interpreter Lock 的缩写,中文通常称为全局解释器锁。它是Python(CPython解释器)中的一个机制,用于同步线程。GIL 确保在任何时刻只有一个线程在执行 Python 字节码。
GIL主要影响的是多线程程序的性能,尤其是在多核处理器上运行时。由于GIL的存在,即使你的计算机有多核处理器,Python 的多线程程序也不能并行地利用多个核心来加速CPU密集型任务。对于I/O密集型的任务,比如网络请求或文件读写等,GIL的影响较小,因为当一个线程等待I/O操作完成时,GIL会被释放,其他线程可以继续执行。
GIL 存在的原因有几个方面:
简化内存管理:Python的内存管理不是线程安全的,GIL可以防止多个线程同时访问和修改同一个对象,从而避免了复杂的线程同步问题。
历史原因:早期的CPython实现中引入了 GIL 来简化实现,并且随着时间的发展,移除GIL需要重写大量的底层代码,这是一项巨大的工程。
性能考虑:在某些情况下,单线程执行可能比多线程上下文切换更高效。
尽管GIL在一定程度上限制了多线程程序的并发性,但Python仍然可以通过多进程或其他方式来充分利用多核处理器的能力。例如,使用multiprocessing模块就可以绕过GIL,每个进程都有自己的Python解释器和GIL,因此可以在不同进程中真正并行地执行代码

框架选择

选择一个合适的框架,应该考虑诸多因素,例如开发效率、团队技术栈、公司自身业务场景、框架自身的可维护性、社区支持和文档等等,不同的框适用于不同的业务场景,选择框架的最终目的是提升开发效率,实现业务需求,框架本身只是一个辅助实现业务逻辑的工具,在FastAPI出来之前,也有很多优秀的框架,例如Bottle、Flask、Django、Sanic等使用非常广泛,各有特色,至今比较流行的如Flask,也是一个轻量级的Web应用框架,它是基于Werkzeug WSGI工具箱和jinja2模板引擎而开发出来的,其自身的维护和插件生态非常好;Django是一个大而全的框架,是一个完整的Web开发框架,对于小业务场景而言用Django会显得比较笨重,部分模块也无法定制开发,然而Django仍然是Python Web框架中非常流行的框架之一,国内很多大企业都是其用户,例如蓝鲸智云PaaS平台、例如Instagram、Pinterest等等,如果需要构建健壮的全栈式Web应用Django是很好的选择

FastAPI是为了构建快速的API而生,其主要特点:

  • 支持ASGI(Asynchronous Server Gateway Interface)协议的Web应用框架,同时兼容ASGI和WSGI
  • 天然支持异步协程处理,能够快速处理更多的HTTP请求
  • 使用了Pydantic类型提示的特性,可以更加高效、快速的进行接口数据类型校验及模型响应等处理
  • 基于Pydantic模型,它还可以自动对响应数据进行格式化和序列化处理
  • 提供以来注入系统的实现,可以更高效的进行代码复用
  • 支持WebSocket、GraphQL等
  • 支持异步后台任务,方便对于耗时的任务进行异步处理
  • 支持服务进程启动和关闭事件回调监听,可以方便的进行插件的扩展初始化
  • 支持跨域请求CORS、压缩Gzip请求、静态文件、流式响应
  • 支持自定义相关中间件来处理请求及相应
  • 支持开箱即用OpenAPI和JSON Schema可以自动生成交互式文档
  • 使用uvloop模块,让原生标准的asyncio内置的事件循环更快

ASGI是异步服务器网关接口,和WSGI一样,都是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口,但是ASGI是WSGI的一种扩展实现,并且提供异步特性和WebSocket等支持,同时ASGI也是兼容WSGI的,在某种程度上可以理解ASGI是WSGI的超集,所以ASGI支持同步和异步同时运行,内部可以通过一个转换装饰器进行相互切换

FastAPI除了融合了原生asyncio异步协程的特性,在CPU的利用率上有比较好的特性外,还基于Starlette和Pydantic做了很多封装,从而简化了开发者的编码工作,例如参数类型提示、校验、直接输出模型响应报文等,都为开发者提供了很多便利,此外FastAPI还配备了OpenAPI规范(OAS),它自动生成了OpenAPI模式,基于OpenAPI用户可以直接快速的对API文档进行查看和调试,可以让开发者使用相同的代码来定义序列化和相关的数据验证

OpenAPI规范(OAS)是一个定义标准的与具体编程语言无关的RESTful API的规范

异步编程

FastAPI框架最大特性就是异步支持,ASGI是一种接口协议,它是为了规范支持异步的Python Web服务器、框架和应用之间的通信而定制的,同时囊括了同步和异步应用的通信规范,并向后兼容WSGI协议,传统的WSGI应用支持单词同步调用,仅在接收一个请求后返回响应,从而无法支持HTTP长轮询或WebSocket链接,在Python3.5增加了async/await特性后,基于asyncio异步协程的应用编程变得更加方便,ASGI协议规范就是用于asyncio框架中底层服务器/应用程序的接口

同步IO编程

同步I/O编程是一种处理输入输出操作的模型,其特点是发起I/O请求后,调用者(如程序中的一个线程)会一直等待直到I/O操作完成。这种模式与异步I/O相对,在异步I/O中,调用者发出请求后可以继续执行其他任务,而不需要等待I/O操作完成。

同步I/O的基本工作流程
  • 发起I/O请求:应用程序向操作系统发出读取或写入数据的请求。
  • 等待I/O完成:应用程序暂停执行,并等待I/O操作的结果。在此期间,该线程不会做任何有用的工作;它被阻塞,直到I/O操作完成。
  • 接收结果:一旦I/O操作完成,操作系统通知应用程序,此时应用程序才能继续执行后续的操作。
同步I/O的特点
  • 简单直观:对于程序员来说,同步I/O模型易于理解和实现。代码按照顺序执行,逻辑清晰。
  • 资源占用:由于每个I/O操作都需要等待完成,因此在处理大量并发连接时,可能会消耗大量的系统资源(如线程),因为每个连接通常需- 要一个单独的线程来处理。
  • 性能问题:如果I/O操作耗时较长,那么使用同步I/O的应用程序响应时间会增加,因为它不能同时处理其他任务。这对于要求高吞吐量和低延迟的应用来说可能是个问题。
  • 可预测性:同步I/O的行为更加确定和可预测,因为它遵循严格的顺序执行原则,这有助于简化错误处理和调试过程。
适用场景
  • 简单的命令行工具:当只需要进行少量且快速的I/O操作时,同步I/O非常适合。
  • 单用户应用:比如桌面应用程序,它们通常不涉及复杂的网络通信,也不需要处理很多并发请求。
  • 开发阶段:在项目初期或原型设计阶段,为了快速搭建功能,开发者可能会选择同步I/O来简化编程复杂度。

总之,同步I/O编程适合于那些对实时性和并发能力要求不高、或者更重视代码简洁性的应用场景。然而,在构建需要处理大量并发连接的服务时,采用异步I/O或其他优化策略通常是更好的选择。

异步IO编程

异步I/O(Asynchronous I/O)是一种处理输入输出操作的模型,它允许程序在发起I/O请求后立即返回并继续执行其他任务,而不需要等待I/O操作完成。一旦I/O操作完成,操作系统会通过某种机制(如回调函数、事件循环或未来对象等)通知应用程序。

异步I/O的基本工作流程
  • 发起I/O请求:应用程序向操作系统发出读取或写入数据的请求,并指定一个回调函数或其他形式的通知机制。
  • 非阻塞返回:应用程序不会被阻塞,而是可以继续执行其他代码逻辑。操作系统开始处理I/O请求。
  • 后台处理I/O:操作系统处理实际的I/O操作。这可能包括从磁盘读取数据、网络通信等。
  • 完成I/O操作:当I/O操作完成后,操作系统通过之前设置的通知机制(例如调用回调函数、触发事件等)来告知应用程序。
  • 处理结果:应用程序接收到通知后,可以处理I/O操作的结果。
异步I/O的特点
  • 非阻塞:应用程序在等待I/O完成期间可以继续执行其他任务,提高了CPU利用率和整体系统性能。
  • 高并发性:适合处理大量并发连接的情况,因为每个I/O操作不需要单独的线程来等待。
  • 复杂度较高:编写和维护异步代码通常比同步代码更复杂,因为它涉及到状态管理和多个执行路径的协调。
  • 错误处理:由于异步操作是分散在整个程序中的,因此需要特别注意异常处理和调试。
  • 响应性:对于用户界面或者实时应用来说,异步I/O能够保持界面的流畅性和响应速度。
适用场景
  • Web服务器:特别是那些需要同时处理成千上万客户端请求的高并发环境。
  • 实时应用程序:比如聊天服务、在线游戏等,这些应用要求快速响应用户操作。
  • 分布式系统:在微服务架构中,服务之间经常需要进行网络通信,使用异步I/O可以提高系统的可扩展性和效率。
  • 大数据处理:在处理大规模数据集时,异步I/O可以帮助加速数据读写过程。
  • 移动应用:移动端资源有限,采用异步I/O可以减少对电池的消耗,同时保证良好的用户体验。

综上所述,异步I/O非常适合于需要高度并发、高性能以及良好响应性的应用场景。虽然实现起来相对复杂一些,但对于现代互联网服务而言,这种额外的努力往往是值得的。

同步IO和异步IO本质区别

同步I/O和异步I/O对CPU和内存的影响主要体现在资源的利用效率、系统的响应能力和可扩展性上。下面是两种I/O模型对CPU和内存的具体影响:

同步I/O对CPU的影响
  • 阻塞等待:当一个线程发起同步I/O请求时,如果I/O操作未完成(例如数据还没有从磁盘读取到内存),该线程将被挂起,进入等待状态。这意味着在等待期间,这个线程不会占用CPU时间片。
  • 上下文切换:如果系统中有其他线程可以执行,则操作系统会进行上下文切换,将CPU时间分配给其他线程。频繁的上下文切换会带来额外的开销。
  • 低效利用:特别是在高并发场景下,大量的线程处于等待状态会导致CPU利用率不高,因为实际能够并行处理任务的能力受限于可用的非阻塞线程数量。
同步I/O对内存的影响
  • 线程堆栈空间:每个等待中的线程都需要保留一定的堆栈空间。随着并发数的增加,总的内存需求也会增加。
  • 缓存使用:同步I/O可能需要为每个连接或文件维护独立的数据缓冲区,这增加了内存消耗。
  • 内存碎片:长期存在的大量线程可能导致内存碎片问题,特别是当这些线程频繁创建和销毁时。
异步I/O对CPU的影响
  • 非阻塞执行:发起异步I/O请求后,线程不会被挂起,而是可以继续执行其他任务,直到I/O操作完成并通过回调等方式通知应用程序。这样可以更有效地利用CPU资源。
  • 减少上下文切换:由于减少了因等待I/O而引起的线程阻塞,因此也减少了不必要的上下文切换,从而降低了开销。
  • 提高吞吐量:对于支持大量并发连接的应用来说,异步I/O能够显著提高整体吞吐量,因为即使是在等待I/O的过程中,系统也能保持活跃状态。
异步I/O对内存的影响
  • 共享资源:多个异步I/O操作可以共用同一组缓冲区,减少了内存中冗余数据的存在。
  • 事件循环机制:通常与异步I/O结合使用的事件循环机制允许更高效地管理内存资源,因为它可以根据需要动态调整内存使用情况。
  • 降低内存压力:相比于为每个连接创建单独线程的方法,采用异步I/O可以大大降低内存的压力,尤其是在高并发环境下。
  • 同步I/O更适合简单且不需要高并发的应用场景,但可能会导致较高的CPU闲置率和较大的内存开销。
  • 异步I/O则适用于高并发、高性能要求的应用环境,它能更好地利用CPU资源,并且通过有效的内存管理来减少内存消耗。
  • 选择哪种I/O模式取决于具体的应用需求、性能目标以及开发团队的经验和技术栈。

异步IO编程形式

异步I/O编程有多种形式,每种形式都有其特定的使用场景和实现方式。以下是几种常见的异步I/O编程模式:

  1. 回调函数(Callback)
    描述:在发起一个I/O操作时,传递一个回调函数作为参数。当I/O操作完成时,操作系统或库会调用这个回调函数,并将结果传递给它。
    示例语言/框架:Node.js, C++中的libuv, Python中的asyncio模块等。
  2. 事件循环(Event Loop)
    描述:通过一个中心化的事件循环来管理所有的I/O操作。程序将I/O任务注册到事件循环中,然后事件循环负责监听这些任务的状态变化,并在适当的时候执行相应的处理逻辑。
    示例语言/框架:Node.js, Python的asyncio, Java的NIO (Non-blocking I/O)等。
  3. 未来对象(Futures/Promises)
    描述:I/O操作返回一个“未来”对象,该对象代表了将来某个时刻可用的结果。程序员可以通过检查或等待这个未来对象来获取最终的I/O结果。
    示例语言/框架:C# (.NET Task), JavaScript Promises, Python的concurrent.futures等。
  4. 协程(Coroutines)
    描述:协程是一种特殊的函数,它可以暂停执行并在稍后恢复。协程允许开发者以同步的方式来编写异步代码,使得代码更易于理解和维护。
    示例语言/框架:Python的asyncio库, C#的async和await关键字, Go语言的goroutines, Kotlin的coroutine等。
  5. 反应式编程(Reactive Programming)
    描述:反应式编程是一种面向数据流和变化传播的编程范式。它关注的是数据的变化,并且能够自动响应这些变化。反应式编程可以很好地与异步I/O结合使用。
    示例库/框架:RxJava (Java), RxJS (JavaScript), Project Reactor (Spring Framework的一部分)等。
  6. 基于通道的通信(Channel-based Communication)
    描述:通过通道(channel)来进行线程间或进程间的通信。发送方将数据写入通道,接收方从通道读取数据。这种方式常用于并发编程中,可以简化异步I/O的设计。
    示例语言/框架:Go语言的channels, Rust的std::sync::mpsc等。
  7. Actor模型
    描述:Actor模型是一种并行计算的高级抽象。每个actor是一个独立的实体,有自己的状态和行为,通过消息传递与其他actor交互。这种模型非常适合构建高并发、分布式的系统。
    示例语言/框架:Erlang/Elixir, Akka (Scala/JVM)等。

选择哪种异步I/O编程形式取决于具体的应用需求、开发团队的技术背景以及所使用的编程语言支持。例如,对于Web开发来说,Node.js的事件驱动模型非常流行;而在需要进行复杂并发控制的系统中,可能更倾向于使用Actor模型或基于协程的方法。

多线程IO编程和多进程IO编程

从多线程和多进程的角度来看,同步I/O和异步I/O的实现方式及其对系统资源(如CPU和内存)的影响有着显著的不同。下面是这两种I/O模型在多线程和多进程环境下的解释:

多线程环境同步I/O
  • CPU利用率:当一个线程执行同步I/O操作时,如果I/O设备还未准备好数据或目标不可达,该线程将被阻塞,等待I/O完成。这意味着在这段时间内,这个线程不会消耗CPU时间。然而,这会导致CPU利用率下降,特别是在高并发场景下,大量的线程可能同时处于阻塞状态。
  • 上下文切换:由于线程经常因为I/O操作而进入等待状态,操作系统需要频繁地进行上下文切换,以调度其他就绪的线程来运行。这增加了额外的开销。
  • 内存使用:每个线程都需要一定的栈空间,即使是在等待I/O的过程中也是如此。因此,随着并发数的增加,总的内存占用也会相应增加。
多线程环境异步I/O
  • CPU利用率:异步I/O允许线程在发起I/O请求后继续执行其他任务,而不是被阻塞。这样可以更有效地利用CPU资源,提高系统的整体吞吐量。
  • 上下文切换:减少了因I/O阻塞导致的线程切换,从而降低了上下文切换的频率和相关的性能开销。
  • 内存使用:通过共享资源和更有效的内存管理(例如事件循环机制),异步I/O通常能够减少内存占用。此外,它不需要为每个I/O操作创建单独的线程,进一步节省了内存。
多进程环境同步I/O
  • CPU利用率:与多线程类似,当进程执行同步I/O操作时,它会被阻塞直到I/O完成。这同样会导致CPU利用率下降,尤其是在多进程环境下,多个进程可能同时被阻塞。
  • 上下文切换:操作系统会在进程间进行上下文切换,以确保所有就绪的进程都有机会运行。这种切换会带来开销。
  • 内存使用:每个进程都有独立的地址空间,这意味着即使是相同的代码和数据,在不同的进程中也需要复制一份。因此,多进程模式下的内存使用通常比多线程更高。
多进程环境异步I/O
  • CPU利用率:与多线程环境中的异步I/O类似,多进程环境中的异步I/O也允许进程在发起I/O请求后继续执行其他工作,从而提高了CPU利用率。
  • 上下文切换:减少了不必要的上下文切换,因为进程不再需要因为I/O操作而频繁挂起和恢复。
  • 内存使用:虽然每个进程仍然有独立的地址空间,但通过有效的内存管理和资源共享,异步I/O可以在一定程度上优化内存使用。不过,相比于多线程,多进程环境下的内存使用通常还是较高。
  • 多线程:适合于需要共享大量数据的应用,并且可以更容易地实现细粒度的并行处理。对于I/O密集型应用,采用异步I/O可以有效提升性能。
  • 多进程:提供了更好的隔离性和稳定性,适合于那些需要高可靠性的服务。但由于每个进程拥有独立的内存空间,所以在内存使用上不如多线程高效。对于I/O密集型应用,同样可以通过异步I/O来改善性能。
  • 选择多线程还是多进程取决于具体的应用需求、系统的设计以及对稳定性和性能的要求。在实际应用中,有时也会结合两者的优势,例如在一个多进程架构中使用多线程来处理具体的I/O操作。

在现在的软件开发中,多线程和多进程非常常见,,他们在某种程度上实现了多任务的并发执行,对于IO型任务,通常通过多线程调度来实现表面上的并发,而对于计算密集型任务,则使用多进程来实现并发,本质上绝对的并发是不存在的,无论是多线程还是多进程

Python的多进程和多线程缺点

Python在多进程编程中

  • 进程的创建和销毁代价非常高
  • 需要开辟更多的虚拟空间
  • 多进程之间上下文的切换时间长
  • 需要考虑多进程之间的同步问题

Python在多线程编程中

  • 每一个线程都包含一个内核调用栈和CPU寄存器上下文列表
  • 共享同一个进程空间会设计同步问题
  • 线程之间上下文的切换需要消耗时间
  • 受限于GIL,在Python进程中只允许一个线程处于运行状态,多线程无法充分利用CPU多核
  • 受OS调度管制,线程是抢占式多任务并发的

并发与并行

并发通常是指在单核CPU情况下可以同时运行多个任务,然而本质上,操作系统在单核CPU下,在处理任务时任一时刻点都只有一个程序在CPU中运行,人之所以感知多任务同时执行,因为操作系统给每个任务都分配了一定的时间片,每个任务执行完分配的时间片后,操作系统会通过调度切换到下一个任务中去执行,而这个时间片对于人而言短到无法感知,就会感觉系统在并发处理任务,实际上是多任务交叉执行的

并行通常对于单核的CPU而言,任务的执行就不会存在并行的情况,如果是多核CPU任务就可以分配到不同的CPU核心上执行,这种情况下多个CPU核心执行的任务互不干扰,互不影响,这是真正的多任务同时执行,也是真正的并行表现

由于一个系统运行的任务数量远超CPU数量,所以在操作系统中没有绝对的真正并行的任务

同步与异步

同步(Synchronous)和异步(Asynchronous)是计算机科学中用来描述程序执行方式的两个重要概念,它们主要涉及到任务处理、通信以及数据交换等方面

同步:在同步操作中,一个任务必须等待前一个任务完成后才能开始。这意味着如果程序中的一个部分正在执行某个操作,那么其他部分需要等待这个操作完成之后才能继续执行。这种模式下,代码按顺序执行,每一行代码都必须等待上一行代码执行完毕后才会被执行。

同步的特点:

  • 简单直观,易于理解和调试。
  • 可能导致阻塞,即当前线程或进程会暂停,直到某个条件满足或者操作完成。
  • 在网络请求等可能耗时较长的操作中,使用同步可能会造成用户体验下降,因为用户界面可能会冻结,直到响应返回。

想象你在一家餐厅点餐。如果你选择了同步的方式,服务员会记录你的订单,然后你必须坐在那里等待食物准备好再给你端上来,在此期间你不能离开去做别的事情。

异步:与同步相反,异步操作允许程序在等待某项任务完成的同时去执行其他任务。当被调用的任务完成后,通过回调函数、事件处理器或者其他机制通知程序该任务已经结束。这种方式可以提高程序效率,特别是在处理I/O密集型任务如文件读写、网络请求等场景下非常有用。

异步的特点:

  • 非阻塞式设计,提高了系统的并发处理能力。
  • 更好的资源利用率,尤其是在多任务或多用户的环境中。
  • 代码可能更复杂,尤其是对于新手来说,理解回调地狱(Callback Hell)等问题可能会有一定难度。

还是以餐厅为例。这次选择异步服务的话,服务员记下你的订单后你可以先离开去做其他的事情,比如逛逛商场。一旦食物准备好,服务员会打电话通知你来取餐,这样你就不会浪费时间等待了。

阻塞和非阻塞

在计算机编程和操作系统中,“阻塞”(Blocking)和"非阻塞"(Non-blocking)是描述进程或线程在执行I/O操作时行为的术语。这两个概念通常与同步/异步的概念相关联,但它们关注的是不同的方面。

  • 阻塞(Blocking):当一个进程或线程发起一个I/O请求后,如果这个请求不能立即完成(比如读取文件、网络通信等),那么该进程/线程就会暂停其执行并等待I/O操作完成。这种情况下,我们说该I/O操作是阻塞的。这意味着,在等待期间,CPU资源对于这个特定的任务来说是空闲的,因为它没有其他工作可以做。

  • 阻塞的特点:

    • 简单直观,易于实现。
    • 当I/O操作时间较长时,会导致效率低下,因为这期间CPU无法进行其他计算任务。
    • 对于用户界面程序来说,可能会导致界面无响应,用户体验差。

假设你正在使用一个文本编辑器打开一个非常大的文件。如果你尝试保存这个大文件,而这个过程需要一些时间来完成,那么在这段时间内,你可能无法与编辑器进行交互(例如打字或者切换到其他文档),这就是一种阻塞情况。

  • 非阻塞(Non-blocking):相比之下,非阻塞I/O意味着进程/线程不会因为I/O操作而被挂起。相反,它会立刻返回控制权给调用者,即使I/O操作还没有完成。这种方式允许程序在等待I/O结果的同时继续处理其他事务。一旦I/O操作准备好,程序可以通过轮询或者其他机制(如事件通知)得知,并且能够及时地对结果做出反应。

  • 非阻塞的特点:

    • 提高了系统的并发性和响应性。
    • 更好地利用了系统资源,特别是在多任务环境中。
    • 实现起来相对复杂,尤其是在管理多个同时进行的I/O操作时。

考虑同样的文本编辑器例子,但是这次编辑器采用了非阻塞方式处理文件保存。当你点击保存按钮后,你可以立刻继续编辑文档或是执行其他操作,而不需要等到保存操作彻底结束。后台会有一个单独的过程负责处理实际的保存工作,当保存完成后,通过某种机制(比如弹出提示框)告知用户。

总之,选择阻塞还是非阻塞取决于具体的应用场景和需求。非阻塞模式适合那些需要高并发性能及良好响应性的应用,而阻塞模式则更适合简单的、顺序化的程序设计。在实践中,许多现代应用程序和服务端框架都倾向于采用非阻塞的方式来优化性能和用户体验。

asyncio协程

不论是多线程还是多进程,在Python中所用的并发模式都是假象,并且操作系统针对于多进程和多线程进行操作的过程中,需要消耗的资源比较多,对于早起多数Web框架都是多线程模式来提供并发支持,在这种情况下用户越多创建的线程就越多消耗的硬件资源也就越多,但硬件资源不是无限的,所以不可能无限创建线程或者进程来处理更多的并发任务,因此考虑在单一线程或者进程中同时处理更多的请求,所以一些IO多路复用模型诞生了

虽然IO多路复用模型可以让任务执行时不在阻塞在某个连接上,而是当任务处理有数据到达时(阻塞结束)才触发回调并响应请求,但这种机制依赖于回调,而回调机制使用起来相对复杂,且容易出现链路式回调,编码实现也不够直观,所以这种链路式回调逐渐被协成机制代替

相对于线程来说,协不存在于操作系统中,它只是一种程序级别上的IO调度,可以理解为它对现有的线程进行分片任务处理,线程在代码块之间切换,能够更快的进行上下文切换,减少线程的创建和切换开销,从而得到提升性能的目的

asyncio是Python官方提供的用于构建协程的并发应用库,是FastAPI实现异步特性的重要组成部分,基于asyncio可以在单线程模式下处理更多的并发任务,它是一个异步IO框架,而异步IO其实是基于事件触发机制来实现异步回调的,在IO处理上主要采用了IO复用机制来实现非阻塞操作

asyncio的核心是Eventloop,它以Eventloop为核心来实现协程函数结果的回调,它提供了相关携程任务的注册、取消、执行及回调等方法来实现并发

在Eventloop中执行的任务其实就是定义的各种协程对象,通常每一个协程对象内部都会包含自身需要等待处理的IO任务,当Eventloop处理协程任务时,遇到需要等待处理的IO任务,会自动执行权限切换自动执行下一个协程任务,当上一个IO任务完成时,会在下一次事件循环返回最终等待结果的状态,这种多任务交易轮换的协同处理机制可以有效的提高CPU的使用

asyncio应用

import requests
import time

def take_up_time(func):
    """
    计算函数执行时间的装饰器。
    
    参数:
    func: 被装饰的函数
    
    返回:
    带有执行时间测量的函数包装器
    """
    def wrapper(*args, **kwargs):
        print("开始执行---->")
        now = time.time()
        result = func(*args, **kwargs)
        using = (time.time() - now) * 1000
        print(f"结束执行,消耗时间为:{using}ms")
        return result
    return wrapper

def request_sync(url):
    """
    同步请求给定的URL并返回响应。
    
    参数:
    url: 请求的URL
    
    返回:
    响应对象
    """
    response = requests.get(url)
    return response

@take_up_time
def run():
    """
    循环请求百度首页并测量总执行时间。
    """
    for i in range(0, 50):
        request_sync('https://www.baidu.com')


if __name__ == '__main__':
    run()

这段代码是一个单线程、同步代码,循环请求了50次

# 导入异步HTTP客户端会话库、异步任务库和时间库
import aiohttp, asyncio, time


# 定义一个装饰器函数,用于计算另一个函数的执行时间
def take_up_time(func):
    # 定义一个包装函数来包裹原始函数
    def wrapper(*args, **kwargs):
        # 在函数执行前打印开始信息
        print("开始执行---->")
        # 记录函数开始执行的时间
        now = time.time()
        # 执行原始函数
        result = func(*args, **kwargs)
        # 计算函数执行时间并转换为毫秒
        using = (time.time() - now) * 1000
        # 打印函数执行结束和消耗的时间
        print(f"结束执行,消耗时间为:{using}ms")
        # 返回原始函数的执行结果
        return result

    # 返回包装函数以替代原始函数
    return wrapper


# 定义一个异步函数,用于发起异步HTTP请求
async def request_async():
    # 使用异步上下文管理器创建一个异步HTTP会话
    async with aiohttp.ClientSession() as session:
        # 使用异步上下文管理器发起一个GET请求到百度
        async with session.get('https://www.baidu.com') as resp:
            pass


# 定义一个装饰器修饰的函数,用于运行多个异步请求任务
@take_up_time
def run():
    # 创建并确保49个异步任务在未来执行
    tasks = [asyncio.ensure_future(request_async()) for x in range(0, 49)]
    # 获取当前的事件循环
    loop = asyncio.get_event_loop()
    # 将所有任务聚合到一起
    tasks = asyncio.gather(*tasks)
    # 运行事件循环直到所有任务完成
    loop.run_until_complete(tasks)


# 如果是主程序入口,则调用run函数开始执行
if __name__ == '__main__':
    run()

这段代码使用了异步请求,通过协程完成并发,执行结果性能差距一目了然


原文地址:https://blog.csdn.net/dawei_yang000000/article/details/142594125

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!