• 流畅的python第十八章使用asyncio包处理并发


    对比一个简单的多线程程序和对应的 asyncio 版,说明多线程和异步任务之间的关

    asyncio.Future 类与 concurrent.futures.Future 类之间的区别
    摒弃线程或进程,如何使用异步编程管理网络应用中的高并发
    在异步编程中,与回调相比,协程显著提升性能的方式
    如何把阻塞的操作交给线程池处理,从而避免阻塞事件循环
    使用 asyncio 编写服务器,重新审视 Web 应用对高并发的处理方式
    为什么 asyncio 已经准备好对 Python 生态系统产生重大影响

    线程与协程对比

    import threading
    import itertools
    import time
    import sys
    
    
    class Signal:
        go = True
    
    
    def spin(msg, signal):
        write, flush = sys.stdout.write, sys.stdout.flush
        for char in itertools.cycle('|/-\'):
            status = char + ' ' + msg
            write(status)
            flush()
            write('x08' * len(status))
            time.sleep(.1)
            if not signal.go:
                break
        write(' ' * len(status) + 'x08' * len(status))
    
    def slow_function():
        time.sleep(3)
        return 42
    
    def supervisor():
        signal = Signal()
        spinner = threading.Thread(target=spin, args=('thinking!', signal))
        print('spinner object:', spinner)
        spinner.start()
        result = slow_function()
        signal.go = False
        spinner.join()
        return result
    
    def main():
        result = supervisor()
        print('Answer:', result)
    
    if __name__ == '__main__':
        main()

    以上是threading

    import asyncio
    import itertools
    import sys
    
    
    @asyncio.coroutine
    def spin(msg):
        write, flush = sys.stdout.write, sys.stdout.flush
        for char in itertools.cycle('|/-\'):
            status = char + ' ' + msg
            write(status)
            flush()
            write('x08' * len(status))
            try:
                yield  from asyncio.sleep(.1)
            except asyncio.CancelledError:
                break
        write(' ' * len(status) + 'x08' * len(status))
    
    @asyncio.coroutine
    def slow_function():
        yield from asyncio.sleep(3)
        return 42
    
    @asyncio.coroutine
    def supervisor():
        spinner = asyncio.async(spin('thinking!'))
        print('spinner object:', spinner)
        result = yield from slow_function()
        spinner.cancel()
        return result
    
    def main():
        loop = asyncio.get_event_loop()
        result = loop.run_until_complete(supervisor())
        loop.close()
        print('Answer:', result)
    
    
    if __name__ == '__main__':
        main()

    以上是asyncio

    除非想阻塞主线程,从而冻结事件循环或整个应用,否则不要在 asyncio 协
    程中使用 time.sleep(...)。如果协程需要在一段时间内什么也不做,应该使用
    yield from asyncio.sleep(DELAY)

    使用 @asyncio.coroutine 装饰器不是强制要求,但是强烈建议这么做,因为这样能在
    一众普通的函数中把协程凸显出来,也有助于调试:如果还没从中产出值,协程就被垃圾
    回收了(意味着有操作未完成,因此有可能是个缺陷),那就可以发出警告。这个装饰器
    不会预激协程。

    线程与协程之间的比较还有最后一点要说明:如果使用线程做过重要的编程,你就知道写
    出程序有多么困难,因为调度程序任何时候都能中断线程。必须记住保留锁,去保护程序
    中的重要部分,防止多步操作在执行的过程中中断,防止数据处于无效状态。
    而协程默认会做好全方位保护,以防止中断。我们必须显式产出才能让程序的余下部分运
    行。对协程来说,无需保留锁,在多个线程之间同步操作,协程自身就会同步,因为在任
    意时刻只有一个协程运行。想交出控制权时,可以使用 yield 或 yield from 把控制权
    交还调度程序。这就是能够安全地取消协程的原因:按照定义,协程只能在暂停的 yield
    处取消,因此可以处理 CancelledError 异常,执行清理操作。

    asyncio与concurrent.future的区别

    期物只是调度执行某物的结果。在 asyncio 包
    中,BaseEventLoop.create_task(...) 方法接收一个协程,排定它的运行时间,然后
    返回一个 asyncio.Task 实例——也是 asyncio.Future 类的实例,因为 Task 是
    Future 的子类,用于包装协程。这与调用 Executor.submit(...) 方法创建
    concurrent.futures.Future 实例是一个道理。
    与 concurrent.futures.Future 类似,asyncio.Future 类也提供了
    .done()、.add_done_callback(...) 和 .result() 等方法。前两个方法的用法与
    17.1.3 节所述的一样,不过 .result() 方法差别很大。
    asyncio.Future 类的 .result() 方法没有参数,因此不能指定超时时间。此外,如果
    调用 .result() 方法时期物还没运行完毕,那么 .result() 方法不会阻塞去等待结果,
    而是抛出 asyncio.InvalidStateError 异常。
    然而,获取 asyncio.Future 对象的结果通常使用 yield from,从中产出结果,如示例
    18-8 所示。
    使用 yield from 处理期物,等待期物运行完毕这一步无需我们关心,而且不会阻塞事件
    循环,因为在 asyncio 包中,yield from 的作用是把控制权还给事件循环。
    注意,使用 yield from 处理期物与使用 add_done_callback 方法处理协程的作用一
    样:延迟的操作结束后,事件循环不会触发回调对象,而是设置期物的返回值;而 yield
    from 表达式则在暂停的协程中生成返回值,恢复执行协程。
    总之,因为 asyncio.Future 类的目的是与 yield from 一起使用,所以通常不需要使
    用以下方法。
    无需调用 my_future.add_done_callback(...),因为可以直接把想在期物运行结
    束后执行的操作放在协程中 yield from my_future 表达式的后面。这是协程的一
    大优势:协程是可以暂停和恢复的函数。
    无需调用 my_future.result(),因为 yield from 从期物中产出的值就是结果
    (例如,result = yield from my_future)。
    当然,有时也需要使用 .done()、.add_done_callback(...) 和 .result() 方法。但
    是一般情况下,asyncio.Future 对象由 yield from 驱动,而不是靠调用这些方法驱
    动。

    对协程来说,获取 Task 对象有两种主要方式。
    asyncio.async(coro_or_future, *, loop=None)
      这个函数统一了协程和期物:第一个参数可以是二者中的任何一个。如果是 Future
    或 Task 对象,那就原封不动地返回。如果是协程,那么 async 函数会调用
    loop.create_task(...) 方法创建 Task 对象。loop= 关键字参数是可选的,用于传入
    事件循环;如果没有传入,那么 async 函数会通过调用 asyncio.get_event_loop() 函
    数获取循环对象。
    BaseEventLoop.create_task(coro)
      这个方法排定协程的执行时间,返回一个 asyncio.Task 对象。如果在自定义的
    BaseEventLoop 子类上调用,返回的对象可能是外部库(如 Tornado)中与 Task 类兼容
    的某个类的实例。

    使用 asyncio 包时,我们编写的异步代码中包含由 asyncio 本身驱动的
    协程(即委派生成器),而生成器最终把职责委托给 asyncio 包或第三方库(如
    aiohttp)中的协程。这种处理方式相当于架起了管道,让 asyncio 事件循环(通过我
    们编写的协程)驱动执行低层异步 I/O 操作的库函数。

    import asyncio
    
    import aiohttp
    
    from ..chapter17.flags import BASE_URL, save_flag, show, main
    
    
    @asyncio.coroutine
    def get_flag(cc):
        url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
        resp = yield from aiohttp.request('GET', url)
        image = yield from resp.read()
        return image
    
    @asyncio.coroutine
    def download_one(cc):
        image = yield from get_flag(cc)
        show(cc)
        save_flag(image, cc.lower() + '.gif')
        return cc
    
    def download_many(cc_list):
        loop = asyncio.get_event_loop()
        to_do = [download_one(cc) for cc in sorted(cc_list)]
        wait_coro = asyncio.wait(to_do)
        res, _ = loop.run_until_complete(wait_coro)
        loop.close()
    
        return len(res)
    
    
    if __name__ == '__main__':
        main(download_many)

    有两种方法能避免阻塞型调用中止整个应用程序的进程:
    在单独的线程中运行各个阻塞型操作
    把每个阻塞型操作转换成非阻塞的异步调用使用

    现在你应该能理解为什么 flags_asyncio.py 脚本的性能比 flags.py 脚本高 5 倍了:flags.py
    脚本依序下载,而每次下载都要用几十亿个 CPU 周期等待结果。其实,CPU 同时做了很
    多事,只是没有运行你的程序。与此相比,在 flags_asyncio.py 脚本中,在
    download_many 函数中调用 loop.run_until_complete 方法时,事件循环驱动各个
    download_one 协程,运行到第一个 yield from 表达式处,那个表达式又驱动各个
    get_flag 协程,运行到第一个 yield from 表达式处,调用 aiohttp.request(...)
    函数。这些调用都不会阻塞,因此在零点几秒内所有请求全部开始。
    asyncio 的基础设施获得第一个响应后,事件循环把响应发给等待结果的 get_flag 协
    程。得到响应后,get_flag 向前执行到下一个 yield from 表达式处,调用
    resp.read() 方法,然后把控制权还给主循环。其他响应会陆续返回(因为请求几乎同
    时发出)。所有 get_ flag 协程都获得结果后,委派生成器 download_one 恢复,保存
    图像文件。

    因为异步操作是交叉执行的,所以并发下载多张图像所需的总时间比依序下载少得多。我
    使用 asyncio 包发起了 600 个 HTTP 请求,获得所有结果的时间比依序下载快 70 倍。

    关于concurrent.future模块以及asyncio模块的内容不容易理解,需要查阅其他资料,另写一篇博文。

  • 相关阅读:
    组合模式
    HashMap,ArrayList扩容
    Maven入门使用(一)
    OutputStreamWriter API 以及源码解读
    java.io.BufferedWriter API 以及源码解读
    java.io.writer API 以及 源码解读
    自定义redis序列化工具
    策略模式
    Spring下redis的配置
    简单工厂模式
  • 原文地址:https://www.cnblogs.com/lgh344902118/p/8393643.html
Copyright © 2020-2023  润新知