FastAPI Web开发入门、进阶与实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.2 异步编程基础

我们知道FastAPI框架的最大特性就是异步支持,在深入FastAPI框架的应用之前,需要先简单了解一些关于异步编程方面的知识。

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

1.2.1 并发编程机制

通常,计算机的任务主要分为两种,一种是计算型密集任务,另一种则是IO密集型任务(如输入/输出阻塞、磁盘IO、网络请求IO)。程序处理并发问题的常见方案是多线程和多进程,那么为什么需要使用多线程和多进程方式来实现并发呢?这就需要回到同步IO编程模式的问题上。

同步IO编程中,由于CPU处理任务计算的速度远高于内存执行任务的速度,所以会遇到IO阻塞引发的执行效率低的问题。即当业务逻辑执行的是一个IO密集型任务时,由于CPU遇到同步的IO任务,因此当前处理IO任务的线程会被挂起,其他需要CPU执行的代码则会处于等待执行的状态,此时需要等待同步的IO任务执行完成后,CPU才可以继续执行后续的任务,这就造成了CPU使用效率低的问题。

引入多线程和多进程方式在某种程度上可以实现多任务并发执行。线程相互之间独立执行,互不影响。对于IO型任务,通常通过多线程调度来实现表面上的并发;对于计算密集型任务,则使用多进程来实现并发

其实,无论是多线程还是多进程协程都无法实现真正的并行

虽然引入多线程和多进程方式在某种程度上可以实现多任务并发执行,但是也相应地存在一定的缺点,特别是在Python中,主要体现为:

❑Python多进程并发缺点:

❍进程的创建和销毁代价非常高。

❍需要开辟更多的虚拟空间。

❍多进程之间上下文的切换时间长。

❍需要考虑多进程之间的同步问题。

❑Python多线程并发缺点:

❍每一个线程都包含一个内核调用栈(Kenerl Stack)和CPU寄存器上下文表(该表列出了CPU中的寄存器以及它们的名称、大小、功能和对应的指令等信息)。

❍共享同一个进程空间会涉及同步问题。

❍线程之间上下文的切换需要消耗时间。

❍受限于GIL,在Python进程中只允许一个线程处于运行状态,多线程无法充分利用CPU多核。

❍受OS调度管制,线程是抢占式多任务并发的(需要关心同步问题)。

相对同步IO而生的异步IO,要解决的问题是在处理任务时,若遇到IO阻塞,则会变为非IO阻塞,也就是说遇到IO任务时,CPU不会等待IO任务执行完成,而是直接继续后续任务的执行。从某种程度上,提高了CPU的使用率。

异步IO本身是一种和语言无关的并发编程设计范例,很多语言对它都有相关实现,它是基于一种单进程、单线程的机制来设计的。

异步IO的本质是基于事件触发机制来实现异步回调。在IO处理上主要采用了IO复用机制来实现非阻塞操作,如在众多的Python Web框架中比较流行的tornado框架,就是比较早出现的一个非阻塞式服务器,它的出现就是为了应对C10K的问题处理。tornado之所以能解决C10K的问题,主要是受益于其非阻塞的方式和对epoll的运用,它每秒甚至可以处理数以千计的连接。这种异步非阻塞是在一个单线程内完成的。在一个线程内可以高效处理更多的IO任务,这就是异步IO的魅力所在。

对于上面内容中涉及的术语,一些读者区分起来可能会有些困难。这些术语是理解协程的关键点。理解了这几个术语,有助于理解什么是异步编程,从而加深对异步编程的理解。

1.2.2 并发与并行

并发通常是指在单核CPU情况下可以同时运行多个应用程序。然而本质上,操作系统(单核CPU的情况)在处理任务时任一时刻点都只有一个程序在CPU中运行。人们之所以可以“看到”多个应用程序(多任务)同时执行,是因为操作系统给每个应用程序(任务)都分配了一定的时间片,每个程序(任务)执行完分配的时间片后,操作系统会通过调度切换到下一个任务中去执行,而这个时间片相对于人类来说短到无法被感知,所以就会感觉系统在并发处理相关任务。本质上说,多任务其实是交叉执行的,并发只是一种“假象”。比如,在单核计算机上,当运行QQ客户端后,还会运行微信客户端及其他应用程序,其实这时所有的应用程序都在一个CPU上执行,这些应用是通过时间片调度切换来获取执行权的。

并行是相对于单核CPU而言的。如果计算机是单核的CPU,那么任务的执行就不会存在并行的说法;如果计算机使用的是多核CPU,那么任务就可以分配到不同的CPU上执行,在这种情况下,在多个CPU上执行的任务互不干扰、互不影响,这是真正的多任务同时执行,也是一种真正的并行表现。比如,在双核计算机中,当运行QQ客户端和微信客户端时,有可能QQ客户端运行在CPU1上,而微信客户端运行在CPU2上,它们对CPU的占用是独享的,执行的过程互不影响。

综上所述,读者可以理解为:

❑并行包含了并发,并发是并行的一种特殊表现。

❑并发通常是对单核CPU任务执行过程的一种组织结构描述的说明,并行是对程序执行过程中一种状态的描述,其主要目的是充分利用多核CPU加速任务执行。

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

1.2.3 同步与异步

通常所说的同步(Synchronous),其实是在强调多个任务执行的一个完整过程,其中的某个任务在执行过程中不允许被中断。多个任务的执行必须是协调一致且有序的,某个任务在执行过程中如遇到阻塞,则其他任务需要等待。

相对于同步来说,异步(Asynchronous)强调多个任务可以分开执行,彼此之间互不影响,某一个任务遇到阻塞,其他任务不需要等待,但是任务执行的结果依然是保持一致的。如果一个任务被分为多个任务单元,那么这些任务单元都是可以分开执行的,多任务单元的执行可以是无序的。

这里类比煮米饭这个任务来理解上述两个概念。这个任务可以被分为如下几个步骤来执行:洗锅→下米→放水→点火→煮熟米饭。

同步来处理,则对于上面的每一个步骤,人们必须按顺序一步一步地完成,中途不可以做其他的事情,即便是“从点火到等待煮熟米饭”这段时间内都不允许做其他事情。这样问题就很明显了,“从点火到等待煮熟米饭”这段时间有10~20min,这段时间只能等,不能做其他事情,很浪费时间。而用异步来处理,则“从点火到等待煮熟米饭”这段时间不需要一直等待,可以去刷剧,此时刷剧和等待煮熟米饭就是异步完成的。

综上所述,读者可以这样理解:

❑同步和异步是程序“获得关注消息”通知的机制,是与消息的通知机制有关的一种描述。

❑同步和异步是一种线程处理方式或手段,它们的区别是遇到IO请求是否等待。

❍同步:代码调用IO操作时,必须等待IO操作完成才返回。

❍异步:代码调用IO操作时,不必等待IO操作完成就可返回。

❍异步操作是可以被阻塞的,只不过它不是在处理消息时被阻塞,而是在等待消息通知时被阻塞。

1.2.4 阻塞与非阻塞

阻塞(Blocking)和非阻塞(Nonblocking)都是针对CPU对线程的调度来说的。当调用的函数(任务)遇到IO时会进行线程挂起的操作,此时就需要等待返回IO执行结果,在等待的过程中无法处理其他任务,称此时的任务执行操作处于阻塞状态。比如,使用requests库请求网页地址,从提交请求处理到等待响应报文返回时执行的操作就是处于阻塞状态,这个请求等待服务器响应返回的过程是一个同步请求的处理过程,因为在这个执行过程中不可以去处理其他任务。

当调用的函数遇到IO时,若不会进行线程挂起的操作,则不需要等待返回IO执行结果,此时可以去做其他任务,称此时的任务执行操作处于非阻塞状态。

综上所述,读者可以这样理解:

❑阻塞和非阻塞描述的是程序的运行状态,表示的是程序在等待消息(无所谓同步或者异步)时的状态。

❑阻塞和非阻塞是线程的状态,线程要么处于阻塞状态,要么处于非阻塞状态,两者并不冲突。它们的主要区别是在数据没准备好的情况下调用函数时当前线程是否立即返回。

❍阻塞:调用函数时当前线程被挂起。

❍非阻塞:调用函数时当前线程不会被挂起,而是立即返回。