如果我具有执行大量计算的功能,并且可能需要一段时间,那么在计算的各个部分之间使用asyncio.sleep()
来释放事件循环(防止阻塞事件循环)是否很好?
import asyncio
async def long_function(a, b, c):
# some calculations
await asyncio.sleep(0) # release event loop
# some another calculations
await asyncio.sleep(0) # release event loop
还有另一种更好的方法来解决此类问题吗?也许有一些最佳做法?
答案 0 :(得分:2)
TL; DR仅使用loop.run_in_executor
进行阻止工作
要了解为什么它无济于事,我们首先制作一个class
来处理事件循环。喜欢:
class CounterTask(object):
def __init__(self):
self.total = 0
async def count(self):
while True:
try:
self.total += 1
await asyncio.sleep(0.001) # Count ~1000 times a second
except asyncio.CancelledError:
return
如果事件循环完全打开,则每秒仅会计数约1000次。
为了演示最坏的方法,让我们开始执行计数器任务并天真地运行一个昂贵的函数,而无需考虑后果:
async def long_function1():
time.sleep(0.2) # some calculations
async def no_awaiting():
counter = CounterTask()
task = asyncio.create_task(counter.count())
await long_function1()
task.cancel()
print("Counted to:", counter.total)
asyncio.run(no_awaiting())
输出:
Counted to: 0
那没有做任何计数!注意,我们从来没有等过。此功能只是在执行同步阻止工作。如果计数器本身能够在事件循环中运行,那我们应该算到那时大约有200个。嗯,所以也许我们将其拆分并利用asyncio
将控制权交还给它可以计数的事件循环?让我们尝试一下...
async def long_function2():
time.sleep(0.1) # some calculations
await asyncio.sleep(0) # release event loop
time.sleep(0.1) # some another calculations
await asyncio.sleep(0) # release event loop
async def with_awaiting():
counter = CounterTask()
task = asyncio.create_task(counter.count())
await long_function2()
task.cancel()
print("Counted to:", counter.total)
asyncio.run(no_awaiting())
输出:
Counted to: 1
嗯,我想这在技术上会更好。但这最终表明了这一点:asyncio
事件循环不应进行任何阻止处理。它并非旨在解决这些问题。事件循环无奈地等待着您的下一个await
。但是run_in_executor
确实提供了解决方案,同时使我们的代码保持asyncio
风格。
def long_function3():
time.sleep(0.2) # some calculations
async def in_executor():
counter = CounterTask()
task = asyncio.create_task(counter.count())
await asyncio.get_running_loop().run_in_executor(None, long_function3)
task.cancel()
print("Counted to:", counter.total)
asyncio.run(in_executor())
输出:
Counted to: 164
好多了!通过良好的老式线程方式,我们的循环能够在阻塞函数也执行操作的同时继续进行。