asyncio中使用阻塞函数

7,869 阅读6分钟
原文链接: yangsoon.github.io

问题描述

最近在学习python的异步编程标准库asyncio,在学习期间,想到如果想要在asyncio中使用阻塞的函数调用,但是不阻塞事件循环的当前线程,应该怎么操作?

例如我想在asyncio中使用第三方阻塞调用库requests(当然现在有支持异步操作的aiohttp),或者是想用一些费时的函数计算,亦或是进行io读写。

问题解决

在《流畅的python》中有这样一段话。

函数(例如io读写,requests网络请求)阻塞了客户代码与asycio事件循环的唯一线程,因此在执行调用时,整个应用程序都会冻结。这个问题的解决方法是,使用事件循环对象的 run_in_executor方法。asyncio的事件循环在背后维护着一个ThreadPoolExecutor对象,我们可以调用run_in_executor方法,把可调用对象发给它执行。

这样我们就知道了我们可以通过run_in_executor方法来新建一个线程来执行耗时函数。

函数讲解

因为书中对run_in_executor函数的介绍很少,所以我们先查阅一下官方文档来看一下run_in_executor函数的具体使用方法。

根据官方文档我们可以知道该方法返回一个协程

AbstractEventLoop.run_in_executor(executor, func, *args)
executor 参数应该是一个 Executor 实例。如果为 None,则使用默认 executor。
func 就是要执行的函数
*args 就是传递给 func 的参数

demo

下面我们就用一个简单的例子来演示一下如何使用,通过输出结果我们可以看出5个阻塞调用同时进行,在5秒后所有调用结束。

import asyncio
from time import sleep, strftime
from concurrent import futures
executor = futures.ThreadPoolExecutor(max_workers=5)
async def blocked_sleep(name, t):
    print(strftime('[%H:%M:%S]'),end=' ')
    print('sleep {} is running {}s'.format(name, t))
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(executor, sleep, t)
    print(strftime('[%H:%M:%S]'),end=' ')
    print('sleep {} is end'.format(name))
    return t
async def main():
    future = (blocked_sleep(i, i) for i in range(1, 6))
    fs = asyncio.gather(*future)
    return await fs
loop = asyncio.get_event_loop()
results = loop.run_until_complete(main())
print('results: {}'.format(results))

输出结果是

[19:49:32] sleep 3 is running 3s
[19:49:32] sleep 4 is running 4s
[19:49:32] sleep 1 is running 1s
[19:49:32] sleep 5 is running 5s
[19:49:32] sleep 2 is running 2s
[19:49:33] sleep 1 is end
[19:49:34] sleep 2 is end
[19:49:35] sleep 3 is end
[19:49:36] sleep 4 is end
[19:49:37] sleep 5 is end
result: [1, 2, 3, 4, 5]

strftime函数是为了格式化输出当前时间,比较清楚的看到调用过程。blocked_sleep函数通过使用run_in_executor方法调用阻塞的sleep()函数。

在官网中有这样一段话 调用协程不会使其中的代码运行,协程对象在被排定执行时间之前都不会进行任何操作。下面有两种基本的方式来启动它的运行:

  1. 在另一个协程中调用 await coroutineyield from coroutine (假定另一个协程已经在执行,即在事件循环中)
  2. 使用 ensure_future 函数或 AbstractEventLoop.create_task 方法来排定执行时间。

根据上面的函数讲解我们已经知道run_in_executor方法返回一个协程。因此我们在blocked_sleep函数中驱动他的执行。

在main函数中future = (blocked_sleep(i, i) for i in range(1, 6))我们产生一个生成器表达式,每个元素都是一个协程。我们将future传递给gather函数。

对于gather函数的使用方法如下:

asyncio.gather(*coros_or_futures, loop=None, return_exceptions=False)
你现在知道gather返回一个包含future对象结果的list即可

python从3.5开始就引入了新的语法 async 和 await 但是之前因为使用yield from习惯了,所以下面来一个之前的版本。大致上和上面的例子一样,有兴趣可以看一下。

import asyncio
from time import sleep, strftime
from concurrent import futures
def blocked(t):
    print(strftime('[%H:%M:%S]'),end=' ')
    print('{} sleep:{}s....'.format(t, t))
    sleep(t)
    print(strftime('[%H:%M:%S]'),end=' ')
    print('{} finished'.format(t))
    return t
    
@asyncio.coroutine
def main():
    with futures.ThreadPoolExecutor(max_workers=5) as executor:
        loop = asyncio.get_event_loop()
        future = [loop.run_in_executor(executor,blocked, i) for i in range(1, 6)]
        fs = asyncio.wait(future)
        return (yield from fs)
        
loop = asyncio.get_event_loop()
results, _ = loop.run_until_complete(main())
print('results: {}'.format([result.result() for result in results]))

输出结果

[20:18:13] 1 sleep:1s....
[20:18:13] 2 sleep:2s....
[20:18:13] 3 sleep:3s....
[20:18:13] 4 sleep:4s....
[20:18:13] 5 sleep:5s....
[20:18:14] 1 finished
[20:18:15] 2 finished
[20:18:16] 3 finished
[20:18:17] 4 finished
[20:18:18] 5 finished
results: [3, 2, 1, 4, 5]

在第二份代码里,我故意使用wait函数来等待任务结束,是为了记录一下不同的函数调用方法,和gather函数不同,wait函数需要传入一个list,并且返回两组Futures,(done, pending)。这就是为什么代码里使用 results, _ = loop.run_until_complete(main())的原因了。

下面是一个使用asyncio.as_comleted方法的例子,该方法返回一个协程迭代器。迭代时迭代器只返回已经完成的future。源码中内部维护一个队列,每次迭代都从队列中返回已经完成的future的结果(result or exception),可以注意到在输出结果中,7秒后,所以任务才完成。因为executor大小设置为5,每次只有5个线程在跑,所以在第一个block运行结束后,我们可以看到第6个block立即执行。

import asyncio
from time import sleep, strftime
from concurrent import futures
def blocked(t):
    print(strftime('[%H:%M:%S]'),end=' ')
    print('{} sleep:{}s....'.format(t, t))
    sleep(t)
    print(strftime('[%H:%M:%S]'),end=' ')
    print('{} finished'.format(t))
    return t
@asyncio.coroutine
def main():
    with futures.ThreadPoolExecutor(max_workers=5) as executor:
        loop = asyncio.get_event_loop()
        future = [loop.run_in_executor(executor,blocked, i) for i in range(1, 7)]
        fs = asyncio.as_completed(future)
        results = []
        for f in fs:
            result = yield from f
            results.append(result)
        return results
  
loop = asyncio.get_event_loop()
results= loop.run_until_complete(main())
print('results: {}'.format(results))

输出结果

[13:42:39] 1 sleep:1s....
[13:42:39] 2 sleep:2s....
[13:42:39] 3 sleep:3s....
[13:42:39] 4 sleep:4s....
[13:42:39] 5 sleep:5s....
[13:42:40] 1 finished
[13:42:40] 6 sleep:6s....
[13:42:41] 2 finished
[13:42:42] 3 finished
[13:42:43] 4 finished
[13:42:44] 5 finished
[13:42:46] 6 finished
results: [1, 2, 3, 4, 5, 6]

总结

在asyncio中调用阻塞函数时,需要使用asyncio维护的线程池来另开线程运行阻塞函数,防止阻塞事件循环所在的线程。

几个重要函数比较

函数 传参 返回值 返回值顺序 函数意义
asyncio.gather 可以传递多个协程或者Futures,函数会自动将协程包装成task,例如协程生成器。 包含Futures结果的list 按照原始顺序排列 注重收集结果,等待一堆Futures并按照顺序返回结果
asyncio.wait a list of futures 返回两个Future集合 (done, pending) 无序(暂定) 是一个协程等传给他的所有协程都运行完之后结束,并不直接返回结果
asyncio.as_completed a list of futures 返回一个协程迭代器 按照完成顺序 返回的迭代器每次迭代只返回已经完成的Futures