Fluent Python - Part18 使用 asyncio 包处理并 发

前几章讨论的是并行,即一次做多件事,对应的就是多线程或多进程。而本章讨论的内容是并发,即一次处理多件事,也就是协程。

本章介绍 asyncio 包,这个包使用事件循环驱动的协程实现并发。主要话题有:

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

首先,本章通过简单的示例来对比 threading 模块和 asyncio 包。

线程与协程对比

首先来看一个简单但有趣的示例:在长时间计算的过程中,使用 multiprocessing 包在控制台中显示一个由 ASCII 字符 ”|/-\“ 构成的动画旋转指针。

现在有两种实现,一个借由 threading 模块使用线程实现,一个借由 asyncio 包使用协程实现。

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))
    flush()

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()

Python 没有提供终止线程的API,这是有意为之的。若想关闭线程,必须给线程发送消息。这里,我使用的是 signal.go 属性:在主线程中把它设为 False 后,spinner 线程最终会注意到,然后干净地退出。

下面来看如何使用 @asyncio.coroutine 装饰器替代线程,实现相同的行为。

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()

这两种 supervisor 实现之间的主要区别概述如下:

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

线程与协程之间的比较还有最后一点要说明:如果使用线程做过重要的编程,你就知道写出程序有多么困难,因为调度程序任何时候都能终端线程。必须记住保留锁,去保护程序中重要部分,防止多步操作在执行的过程中中断。

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

使用 asyncioaiohttp 包下载

import asyncio
import sys
import os
import aiohttp
import time
BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = './downloads/'
POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
            'MX PH VN ET EG DE IR TR CD FR').split()


def show(text):
    print(text, end=' ')
    sys.stdout.flush()

@asyncio.coroutine
def get_flag(cc):
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    print(url)
    resp = yield from aiohttp.request('GET', url)
    image = yield from resp.read()
    return image

def save_flag(img, filename):
    path = os.path.join(DEST_DIR, filename)
    with open(path, 'wb') as fp:
        fp.write(img)

@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)

def main(download_many):
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))

if __name__ == '__main__':
    main(download_many)


在最后,需要额外说明 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 包中的某个协程函数或协程方法,也就是说,最内层的子生成器是库中真正执行 I/O 操作的函数,而不是我们自己编写的函数。

改进 asyncio 下载脚本

这个版本想要添加一些功能:显示进度条以及处理各种异常。

使用 asyncio.as_completed 函数

为了更新进度条,各个协程运行结束后就要立即获取结果。loop.run_until_complete 方法不满足我们的需要,此时,为了集成进度条,我们使用的是 as_completed 生成器函数。

部分代码如下:

@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:
        res = yield from future
        status = res.status
        counter[status] += 1
    return counter

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

Python 社区往往会忽略一个事实—访问本地文件系统会阻塞。在上述示例中,阻塞型函数是 save_flag。解决这个问题的方法是,使用事件循环对象的 run_in_executor 方法。

asyncio 的事件循环在背后维护着一个 ThreadPoolExecutor 对象,我们可以调用 run_in_executor 方法,把可调用的对象发给它执行。

@asyncio.coroutine
def download_one(cc, base_url, semaphore, verbose):
    with (yield from semaphore):
        image = yield from get_flag(base_url, cc)
   loop = asyncio.get_event_loop()
   loop.run_in_executor(None, save_flag, iamge, cc.lower() + '.gif')
   status = HTTPStatus.ok
   msg = 'OK'
   return Result(status, cc)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值