问题描述
最近在学习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()函数。
在官网中有这样一段话 调用协程不会使其中的代码运行,协程对象在被排定执行时间之前都不会进行任何操作。下面有两种基本的方式来启动它的运行:
- 在另一个协程中调用
await coroutine
和yield from coroutine
(假定另一个协程已经在执行,即在事件循环中) - 使用
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 |