zoukankan      html  css  js  c++  java
  • FastAPI 异步代码、并发和并行

    作者:麦克煎蛋   出处:https://www.cnblogs.com/mazhiyong/ 转载请保留这段声明,谢谢!

    我们这里探讨下关于异步代码、并行和并发的一些概念。

    一、初探

    1、如果我们使用必须用await调用的第三方库,例如:

    results = await some_library()

    那么我们就要用async def来定义路径操作函数:

    @app.get('/')
    async def read_results():
        results = await some_library()
        return results

    注意:我们在基于async def定义的函数内部才能使用await

    2、如果第三方库不支持使用await,那么我们就用def定义路径操作函数即可。

    @app.get('/')
    def results():
        results = some_library()
        return results

    3、如果我们的应用不需要与第三方通讯,那么就用async def来定义路径操作函数。

    4、如果我们不知道怎么做,那么就用def来定义路径操作函数。

    无论上述哪种情况,FastAPI都会执行异步工作并且速度极快。

    但如果我们遵循一些操作规范,将会带来一些性能上的优化。

    现代版本的Python通过使用"协程"来实现对"异步代码"的支持,在语法上表现为asyncawait的使用。

    我们以下重点讲述的内容为:

    • 异步代码
    • asyncawait
    • 协程

    二、异步代码

    异步代码通常表示,开发语言有一种方式用来通知计算机(应用)在代码的某个地方,必须等待某些事件在其他地方完成。

    这里的某些事件我们称之为"slow-file"。在等待"slow-file"完成的这个时间段内,计算机可以执行一些别的任务。

    然后计算机(应用)一旦有机会就会返回,比如它需要再次等待,或者它完成了在这个地方的所有其他任务。

    接下来会检查所有等待的任务是否已经完成,或者继续执行应当要完成的任务。

    然后它会从等待任务中取走第一个任务继续执行。

    这里等待的某些事件通常指的是,相对程序计算或者内存操作比较耗时的I/O操作,例如

    • 网络通讯
    • 硬盘文件读写
    • 远程 API 操作
    • 数据库操作
    • 其他耗时操作

    之所以被称为"异步"是因为计算机(应用)没必要为了"同步"等待"slow-file"完成而什么事情都不做,那样的话只能等待取到任务结果后才能继续工作。

    实际上,作为一个异步系统,一旦某些事件完成,这个事件会等待一会以便计算机(应用)返回获取结果,然后利用执行结果继续工作。

    对于同步系统来说,通常也称之为"顺序模型",因为在切换执行一个不同任务的时候,计算机(应用)严格遵循序列中的步骤,即使有些步骤包含了等待。

    2.1 并发汉堡

    以上讨论的异步代码有时候也称之为"并发",这与"并行"是不同的。

    "并发"和"并行"都意味着"不同的事情或多或少在相同的时间发生",但它们的细节是非常不同的。

    你和你的朋友去吃快餐,你排队的时候收银员按顺序为在你之前的顾客点餐。
    轮到你的时候,你为自己和朋友点了两份新潮的汉堡。
    然后你付钱。
    然后收银员告诉厨师以便他准备你的汉堡(虽然他可能正在为其他顾客准备汉堡)。
    收银员给你的订单号。

    在等待取餐的时候,你和朋友挑选一个桌子坐下,然后你们交流了很长时间(制作新潮的汉堡比较耗时)。
    在和朋友愉快交流的时候,时不时的,你会看一下柜台是否显示了你的订单号。

    在某个时间终于轮到你了,你去柜台取回你的汉堡,然后返回到座位和你的朋友分享。
    ---------------------------------------------------------------------------
    在这个故事里,你可以把自己想象成计算机(应用)。

    当你排队的时候你是空闲的,没有做什么有效的工作。但队伍是很快的,因为收银员仅仅是收银和下单。
    轮到你的时候,你做了一些有效的工作,你查看菜单和决定点菜内容,然后支付并检查支付结果,同时确认返回的订单内容是正确的。
    然后,虽然你还没得到汉堡,但是你和收银员之间的工作处于"暂停"状态,因为你不得不等待汉堡制作完成。

    虽然你离开了收银台,带着一个号码返回到了桌子旁,不过你可以把你的注意力切换到你的朋友身上,继续你们的交流"工作"。
    然后你有了一些"有效的"工作,那就是增进你和朋友之间的感情。

    当收银员说汉堡准备好了并且把你的订单号放在显示屏上的时候,你并不会立刻跳起来冲过去,因为你知道没人偷你的汉堡,这是你的订单号,别人有别人的订单号。
    因此你会等待你的朋友讲完她的故事(结束当前的工作),然后微笑着告诉她你要去取汉堡。

    然后你来到柜台(继续你最开始的任务),取到汉堡,感谢收银员,然后返回到座位。现在终于结束了与柜台之间的交互任务。
    按照顺序,现在开启了一个新的任务,"吃汉堡",但前一个任务"取汉堡"已经完成了。

    2.2 并行汉堡

    现在我们来看一下什么是并行汉堡。

    你和你的朋友去获取并行快餐。

    你排队的时候,同时有几个(暂且认为有8个)收银员在为顾客下单,这几个收银员同时也是厨师。
    每个在你前面的顾客必须等待取到汉堡才能离开柜台,因为这8个收银员在为下一个顾客服务之前,必须立刻去准备好当前顾客的汉堡。

    轮到你的时候,你下单两个新潮的汉堡。
    你完成支付。
    然后收银员去厨房制作。
    你在柜台前等待着,这样就不会有别人能取走你的汉堡,因为并没有根据订单号取货。
    这样在你和你的朋友忙于不让别人插队和取走你的汉堡的时候,你们并没有多余的精力进行交流。

    这是一种"同步"工作,你和收银员(厨师)之间处于同步状态。你必须在那里等到收银员(厨师)制作完成汉堡然后交给你,否则别人就可能会取走你的汉堡。
    经过在柜台前的长时间等待后,收银员(厨师)终于带着你的汉堡回来了。
    你取到汉堡然后返回餐桌和朋友一起就餐。
    你享用汉堡。完成你的汉堡任务。

    因为在柜台前大量的等待时间,你并没有与你的朋友有很好的交流。
    ---------------------------------------------------------------------------
    在并发汉堡的场景里,你是一个带有两个处理器(你和你的朋友)的计算机(应用),同时在柜台前等待了很长时间。
    快餐店有8个处理器(收银员/厨师)。与之相比并发快餐店只有两个处理器(一个收银员,一个厨师)。
    但最终,并发汉堡的体验仍然不是最好的。

     下面是一个与汉堡相同的并行故事。

    直到最近,大部分的银行还是有多个收银员和一个长长的队伍。
    所有的收银员都是处理完当前顾客的所有事情后,才会开始服务下一位。
    你不得不在队伍里长时间等待,否则你就会失去你的机会。

    2.3 汉堡结论:

    在"与朋友一起吃快餐汉堡"的场景里,因为有许多时间在等待,这就为并发系统带来了更多意义。
    
    这也是大多数web应用的常见情景。 许多许多用户都在等待通过他们不太好的网络来发送请求,然后又等待请求结果的返回。 这种单次的"等待"虽然是毫秒级的,但把它们加起来,最终就导致了大量的等待。
    这也是在web应用中使用异步代码的实际意义

    许多流行的Python框架(包含Flask和Django)是在Python的新异步特性存在之前创建的,因此它们对异步特性的支持并不像最新的特性那么给力。

    异步特性造就了NodeJS的流行,同时也是Go语言的立足所在。现在我们通过FastAPI框架也能获得同样的性能水平。

    2.4 并发比并行更好吗?

    不是这样的,这并不是这个故事的寓意所在。

    并发与并行是不同的。只有在某些特定的场景下(比如包含大量的等待)它是较好一些的。

    通常对于web应用来说,并发是比并行更好一些的。但这并不是全部。

    想象一下下面这个小故事:

    你要打扫一个又大又脏的房间!

    对了!这才是全部的故事内容!

    在这个房间的所有地方,不存在需要等待的事情,只有大量的工作需要完成。
    你可以像汉堡范例那样按顺序执行,先打扫起居室,然后打扫厨房,但因为不存在等待,只是不停的打扫和打扫,因此打扫的顺序不会有任何影响。
    无论是否按照顺序或者不按照顺序(并发)打扫,你需要完成的全部工作量是相同的,你所花费的全部工时也是相同的。

    但是在这种情况下,如果你能带来8个人(以前叫收银员/厨师,现在叫清洁工人),每个人(和你一起)各自负责打扫房间的一块区域,你们就能并行的完成所有的工作,并且更加快速。
    这个时候,每一个清洁者(包括你自己)就是一个处理器,各自完成各自负责的工作。

    因为大部分的执行时间被实际工作所占据,并且在计算机中实际工作是被CPU所完成的,我们通常称这类问题为"CPU bound",也称之为计算密集型。
    ---------------------------------------------------------------------------
    计算密集型的操作通常涉及到复杂的数学计算。
    例如多媒体的处理、机器视觉、机器学习或者深度学习等

    我们可以这样简单理解并发与并行:

    你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
    你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
    你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
    并发的关键是你有处理多个任务的能力,不一定要同时。
    并行的关键是你有同时处理多个任务的能力。 

    2.5 并发 + 并行: web和机器学习

    借助FastAPI,我们通常可以在web开发中充分利用并发的优势。

    但对于计算密集型的工作(比如机器学习)我们也可以利用到并行和多处理器的优势。

    尤其考虑到Python是数据计算、机器学习以及深度学习的主要语言,这也使得FastAPI非常适用于数据计算/机器学习的web API和应用开发。

    三、async and await

    现代版本的Python用一种非常直观的方式来定义异步代码。看起来就像通常的"顺序"代码在某一时刻进行"等待"操作。

    当有一个操作需要等待执行结果的时候,代码范例如下:

    burgers = await get_burgers(2)

    关键之处在于使用了await。这里告诉Python必须等待get_burgers(2)执行完,才能把结果存储到burgers中。

    借助上述语法,Python将会在这个期间内去执行别的操作(比如接收新的请求)。

    await必须在一个支持异步特性的函数内进行使用,也就是说必须是用async def声明的函数:

    async def get_burgers(number: int):
    # Do some asynchronous stuff to create the burgers
        return burgers

    而不是用def声明的函数:

    # This is not asynchronous
    def get_sequential_burgers(number: int):
    # Do some sequential stuff to create the burgers
        return burgers

    借助async def,Python知道在这样的函数内,必须要注意await表达式。它可以在结果返回前,"暂停"这个函数的执行,先去执行别的任务。

    当你调用async def函数的时候,你也必须等待(await)它,否则函数将不会执行。

    # This won't work, because get_burgers was defined with: async def
    burgers = get_burgers(2)

    因此,当我们使用的第三方库声明要使用await调用的时候,我们必须要创建一个基于async def声明的路径操作函数,例如:

    @app.get('/burgers')
    async def read_burgers():
        burgers = await get_burgers(2)
    return burgers

    相关技术细节

    我们注意到了await操作必须是在async def函数内使用,async def函数也必须在另一个async def函数内使用。

    这就像鸡生蛋和蛋生鸡的问题,我们怎么调用第一个async函数呢?

    在FastAPI框架内部我们不用担心这个问题,因为第一个函数就是我们的路径操作函数,而FastAPI框架会正确处理这个问题。

    在FastAPI框架外关于 async/await 的详细使用,我们可以参考文档 check the official Python docs

    四、关于协程

    对于async def函数返回的操作,我们有一个非常花式的术语称之为"协程"。

    Python知道这个操作可以像函数一样启动和完成,并且也可以在内部暂停执行,只要存在await操作。

    通过asyncawait完成的异步代码通常称为"协程",这是相对比于Go语言的主要特性,其称之为"Goroutines"。

    五、其他技术细节

    5.1 路径操作函数

    当直接通过def声明一个路径操作函数的时候,它会运行在一个外部的线程池里并且处于等待状态,而不是被直接调用(这样往往会阻塞住整个server)。

    如果我们以前使用的是与上述工作方式不同的异步框架,并且习惯了直接定义def函数来获取微小的性能提升(也许是100纳秒),请注意在FastAPI中

    这种效果是完全不同的。在这种情况下,我们最好用async def来声明函数除非路径操作函数在执行I/O阻塞操作。

    无论在哪种情况下,FastAPI都会比你以前所用的框架表现更好一些(或者至少是持平)。

    5.2 依赖项

    如果依赖项函数是def而不是async def定义的,那么它也运行在外部的线程池中。

    你可能有多个依赖项和子依赖项相互依赖,一些是async def定义的,一些是def定义的。这仍然会正常工作。def定义的会在外部线程中被调用。

    5.3 工具函数

    其他直接调用的工具类函数,无论是async def定义或者是def定义,FastAPI不会影响你调用的方式。

    这与FastAPI为你调用的函数是截然不同的:包括了路径操作函数和依赖项函数。

    如果你的工具类函数是def定义的,那么它会被直接调用,而不会运行在任何线程池中;如果是async def定义的,那么当你调用的时候应当使用await操作。

    参考文章:

    https://fastapi.tiangolo.com/async/ 



  • 相关阅读:
    noip模拟赛 寻宝之后
    noip模拟赛 剪纸
    noip模拟赛 天天和不可描述
    noip模拟赛 罪犯分组
    noip模拟赛 天天寄快递
    Uva10562
    Uva10305 Ordering Tasks
    Uva 816 Abbott's Revenge
    Uva1103 Ancient Messages
    Uva297 Quadtrees
  • 原文地址:https://www.cnblogs.com/mazhiyong/p/13391115.html
Copyright © 2011-2022 走看看