python3 asyncio 包的使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yangxiaodong88/article/details/81903042

使用asyncio 包进行并发

抨击线程的往往是系统程序员,他们考虑的使用场景对一般的应用程序员来说,也许
一生都不会遇到……应用程序员遇到的使用场景,99% 的情况下只需知道如何派生
一堆独立的线程,然后用队列收集结果
——Michele Simionato 深度思考 Python 的人

在别的博客中讲了使用concurrent.futures 模块处理并发, 里面也有将阻塞函数变为非阻塞函数。

这里使用asyncio 包来处理并发, 两个中都有期物, future的概念。 期物是基础。

相比较concurrent.futures 而言, asyncio 用于协程实现并发

协程替代多线程实现动画

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():
    # 假装等待I/O一段时间
    # yield from asyncio.sleep(3)
    yield from asyncio.sleep(3)  # 把控制权交给主循环,在休眠结束之后恢复这个协程
    return 5  # 协程中获取的值都是return 中的值


@asyncio.coroutine
def supervisor():
    # asyncio.async() 函数排定spin协程的运行时间, 使用一个task对象包装spin协程并立即返回
    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.coroutine 这个装饰器不会预激协程。

线程函数

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

异步版本

@asyncio.coroutine
def supervisor():

    spinner = asyncio.async(spin('thinking!'))
    print('spinner object:', spinner)
    result = yield from slow_function()
    spinner.cancel()
    return result

比较线程和异步代码之间的区别

  • asyncio.Task 对象差不多与threading.Thread对象等效, Task对象是实现协作式多任务的库
  • Task 对象用于驱动协程, Thread 对象用于调用可调用的对象,
  • Task 对象不由自己动手实例化,而是通过而是通过把协程传给asyncio.async()函数或者loop.create_task() 方法获取
  • 获取到的Task对象已经排定好了运行时间(例如asyncio.async函数排定)。Thread实例必须要调用start方法明确告诉它执行。
  • slow_function() 方法是协程由yield from 驱动。
  • 没有API 可以从外部终止线程,因为线程会随时中断,导致系统会处于无效状态。如果想终止任务, 可以使用Task.cancel()实例方法,在协程内部抛出CancelledError 异常。协程可以在暂停的yield 处捕获这个异常, 处理终止请求
  • supervisor 协程必须在main函数中由 loop.run_until_complete 方法执行

多线程编程中 很困难, 必须记住保留锁, 调度程序的时候随时都能中断线程。 必须记住保留锁, 去保护程序中的重要部分。

而协程会默认做好全方位的保护, 防止中断。我们必须显示的产出才能让程序的剩余部分运行。对于协程来说无需保留锁,在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻只有一个协程在运行。 想交出控制权的时候, 可以使用yield 或者yield from 把控制权交还调度程序。 这就是能安全取消协程的原因: 按照定义, 协程只能在暂停的yield 处取消, 因此可以处理CancelledError异常, 执行清理工作。

asyncio.Future 类和concurrent.futures.Future类之间区别

asyncio.Future 和concurrent.futures.Future 类的接口基本一致, 不过实现方式不一样,不可以互换。

期物只是调度执行某物的结果。 在asyncio 中中, baseEventLoop.create_task() 方法接受一个协程, 排定他的运行时间, 然后返回一个asyncio.Task实例, 也是asyncio.Future类的实例。 因为Task 是 Future的子类, 用于包装协程。这与调用Executor.submit() 方法创建concurrent.futures.Future实例是一个道理。

与concurrent.futures.Future类似, asyncion.Future 类也提供了.done() .add_done_callback(), .result() 方法,前两个方法两者差不多, 但是.result 差别比较大。

asyncio.Future 类的 .result()方法没有参数,因此不能指定超时时间。 如果调用.result() 方法时候期物还没有运行完毕, 那么.resutl() 不会阻塞去等待结果, 而是抛出异常 asyncio.InvalidStateError

但是不用害怕, 获取asyncio.Future对象的结果通常使用yield from, 从中产出结果。

使用yield from 处理期物, 等待期物运行完毕这一步不需要我们操心,而且不会阻塞事件循环, 因为在asyncio包中 yield from 的作用把控制权交给事件循环。

使用yield from 和使用add_done_callback() 方法处理协程的作用一样:
延迟操作结束之后, 事件循环不会触发回调对象, 而是设置期物的返回值;而yield from 的表达式则在暂停的协程中生成返回值, 恢复执行执行协程。

因为asyncio.Future 类的目的是与yield from 一起使用, 所以不需要使用一下方法:

  • 无需调用my_future.add_done_callback(…), 因为可以直接把想在期物运行结束后执行的操作放在协程中 yield from 表达式的后面, 这是协程的一大优势: 协程是可以暂停和恢复的函数
  • 无需调用.result() 方法, 因为yield from 可以从期物中产出的值就是结果。(result = yield from my_future)

当然,有时也需要使用 .done()、.add_done_callback(…) 和 .result() 方法。但
是一般情况下,asyncio.Future 对象由 yield from 驱动,而不是靠调用这些方法驱
动。

从期物任务和协程中产出

在 asyncio 包中,期物和协程关系紧密,因为可以使用 yield from 从
asyncio.Future 对象中产出结果。这意味着,如果 foo 是协程函数(调用后返回协程
对象),抑或是返回 Future 或 Task 实例的普通函数,那么可以这样写:res = yield
from foo()。这是 asyncio 包的 API 中很多地方可以互换协程与期物的原因之一。

为了执行这些操作, 必须排定协程的运行时间, 然后使用asyncio.Task对象包装协程。 对于协程来说, 获Task对象有两种主要的方式。

     asyncio.async(coro_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.async 函数)把参数指定的协
程包装在 asyncio.Task 对象中,例如 BaseEventLoop.run_until_complete(…) 方

asyncio 包下载

import asyncio
import aiohttp
from 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)

可以看出这里是手动关闭的, 这个时候可能想到了 with 代码块确保循环被关闭。 但是实际的情况是很复杂的,客户代码绝对不会直接穿件事件循环, 而是直接调用asyncio.get_event_loop()函数, 获取事件循环的引用。 而且有时候我们得代码不拥有时间循环。 因此关闭事件循环 会出错。

asyncio.wait() 协程的参数是一个由期物或者协程构成的可迭代对象;wait 会分别把各个协程包装进一个task 对象。 最终的结果是, wait处理的所有对象都通过某种方式变成future 的实例。 wait 协程函数, 因此返回的是一个协程或者生成器对象。wait_coro 变量中存储的正式这种对象。 为了驱动协程我们把协程传给loop.run_until_complete() 方法。

loop.run_until_complete 方法的参数是一个期物或者一个协程。如果是协程, run_until_complete 方法和wait() 方法一样, 把协程包裹进一个Task对象中。协程, 期物和任务都能由yield from 驱动。这正是 run_until_complete 方法对 wait
函数返回的 wait_coro 对象所做的事
wait_coro 运行结束后返回一个元组,第一个元
素是一系列结束的期物,第二个元素是一系列未结束的期物。在示例 18-5 中,第二个元
素始终为空,因此我们把它赋值给 _,将其忽略。但是 wait 函数有两个关键字参数,如
果设定了可能会返回未结束的期物;这两个参数是 timeout 和 return_when。详情参见
asyncio.wait 函数的文档(https://docs.python.org/3/library/asyncio-
task.html#asyncio.wait)

yield from

在协程中 yield from 做两点阐述

  • 使用yield from 链接的多个协程最终必须由不是协程的调用方驱动。调用方式或隐(例如for 循环中)在最外层外派生成器上调用next() 函数 或者send() 方法
  • 链条中 最内层的子生成器必须是简单的生成器(只使用yield )或可迭代的对象。

在 asyncio 包的 API 中使用 yield from 时,这两点都成立,不过要注意下述细节。

  • 我们编写的协程链条始终通过把最外层委派生成器传给 asyncio 包 API 中的某个函
    数(如 loop.run_until_complete(…))驱动. 也就是说,使用 asyncio 包时,我们编写的代码不通过调用 next(…) 函数或
    .send(…) 方法驱动协程——这一点由 asyncio 包实现的事件循环去做。

  • 我们编写的协程链条最终通过 yield from 把职责委托给 asyncio 包中的某个协程
    函数或协程方法(例如示例 18-2 中的 yield from asyncio.sleep(…)),或者
    其他库中实现高层协议的协程(例如示例 18-5 中 get_flag 协程里的 resp =
    yield from aiohttp. request(‘GET’, url))。也就是说,最内层的子生成器是库中真正执行 I/O 操作的函数,而不是我们自己编写
    的函数。

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

避免阻塞型调用

可以理解阻塞函数 : 把执行硬盘 或者网络I/O 操作的函数定义为 阻塞型函数。

有两种方法可以避免阻塞型调用终止整个应用程序的进程

  • 把单独的线程中运行各个阻塞型操作
  • 把每个阻塞型操作转换成非阻塞的异步调用使用

多个线程是可以的, 但是各个操作系统线程(Python使用的线程)消耗的内存达到兆字节(具体取决操作系统种类)。 如果要处理几千个连接, 而每个连接需要处理一个线程的话, 我们负担不起

为了降低内存的消耗, 通常使用异步来实现异步调用。使用回调时候,不等待回应, 而是注册一个函数, 在发生某件事时候调用。 这样所有调用都是非阻塞的。

只有异步应用程序底层的事件循环能依靠基础设置的中断, 线程, 轮询和后台进程等, 确保多个并发请求能取得进展并最终完成, 这样才能使用回调。
事件循环获得响应后, 会回过头来调用我们指定的回调。 不过这种做法确实正确, 事件循环和应用代码共用主线程绝不会阻塞。

把生成器当成协程来使用也是异步编程的另外一种方式。对于事件循环来说, 调用回调与在暂停的协程上调用send()方法的效果差不多。 各个暂停的协程是要消耗内存, 但是比线程消耗的内存的数量级小。而且协程能够避免可怕的回调地狱。

但是asyncio包目前没有提供异步文件系统API(Node有)

asyncio.as_completed函数

上面时间循环忽地结果用的是asyncio.wait函数, loop.run_until_complete方法驱动, 注意这个方法驱动的是, 全部协程运行完毕后, 这个函数才会返回所有下载结果。 但是有时候的需求是, 各个协程运行完毕后就要立即获取结果。asyncio.as_comoleted() 生成器函数就是这样的函数。

import asyncio
import collections
import aiohttp
from aiohttp import web
import tqdm
from flags2_common import main, HTTPStatus, Result, save_flag

# 默认设为较小的值,防止远程网站出错
# 例如503 - Service Temporarily Unavailable
DEFAULT_CONCUR_REQ = 5
MAX_CONCUR_REQ = 1000


class FetchError(Exception):
    def __init__(self, country_code):
        self.country_code = country_code


@asyncio.coroutine
def get_flag(base_url, cc):
    url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower())
    resp = yield from aiohttp.request('GET', url)
    if resp.status == 200:
        image = yield from resp.read()
        return image
    elif resp.status == 404:
        raise web.HTTPNotFound()
    else:
        raise aiohttp.HttpProcessingError(
            code=resp.status, message=resp.reason,
            headers=resp.headers)


@asyncio.coroutine
def download_one(cc, base_url, semaphore, verbose):
    """

    :param cc:
    :param base_url:
    :param semaphore: 这个参数是asyncio.Semphore 类的实例。Semaphore类是同步装置, 用于限制并发请求数量
    :param verbose:
    :return:
    """
    try:
        # 把semaphore 当成上下文管理器使用, 防止阻塞整个系统, 如果semaphore计数器是所允许的最大值, 只有这个协程会阻塞
        with (yield from semaphore):
            # 退出这个with 语句之后semaphore计数器的值会递减, 解除阻塞可能在等待同一个semaphore对象的其他协程实例
            image = yield from get_flag(base_url, cc)
    except web.HTTPNotFound:
        status = HTTPStatus.not_found
        msg = 'not found'
    except Exception as exc:
        raise FetchError(cc) from exc

    else:
        save_flag(image, cc.lower() + '.gif')
        status = HTTPStatus.ok
        msg = 'OK'

    if verbose and msg:
        print(cc, msg)
    return Result(status, cc)


@asyncio.coroutine
def downloader_coro(cc_list, base_url, verbose, concur_req):
    counter = collections.Counter()
    semaphore = asyncio.Semaphore(concur_req)
    to_do = [download_one(cc, base_url, semaphore, verbose)
             for cc in sorted(cc_list)]
    to_do_iter = asyncio.as_completed(to_do)
    if not verbose:
        to_do_iter = tqdm.tqdm(to_do_iter, total=len(cc_list))
    for future in to_do_iter:
        try:
            res = yield from future
        except FetchError as exc:
            country_code = exc.country_code
            try:
                error_msg = exc.__cause__.args[0]
            except IndexError:
                error_msg = exc.__cause__.__class__.__name__
            if verbose and error_msg:
                msg = '*** Error for {}: {}'
                print(msg.format(country_code, error_msg))
                status = HTTPStatus.error
        else:
            status = res.status
        counter[status] += 1

    return counter


def download_many(cc_list, base_url, verbose, concur_req):
    loop = asyncio.get_event_loop()
    coro = downloader_coro(cc_list, base_url, verbose, concur_req)
    counts = loop.run_until_complete(coro)
    loop.close()
    return counts


if __name__ == '__main__':
    main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)

使用executor对象, 防止阻塞事件循环

我们不应该忽略, 访问本地文件会阻塞

猜你喜欢

转载自blog.csdn.net/yangxiaodong88/article/details/81903042