我有一个生成任务的函数(io绑定的任务):
def get_task():
while True:
new_task = _get_task()
if new_task is not None:
yield new_task
else:
sleep(1)
我正在尝试用asyncio编写一个使用者,该使用者将同时处理最多10个任务,一个任务完成,然后再处理一个任务。 我不确定是否应该使用信号量,或者是否有任何类型的asycio pool执行器?我开始用线程编写伪代码:
def run(self)
while True:
self.semaphore.acquire() # first acquire, then get task
t = get_task()
self.process_task(t)
def process_task(self, task):
try:
self.execute_task(task)
self.mark_as_done(task)
except:
self.mark_as_failed(task)
self.semaphore.release()
有人可以帮助我吗?我不知道在哪里放置异步/等待关键字
答案 0 :(得分:1)
使用 asyncio.Sepmaphore
的简单任务上限async def max10(task_generator):
semaphore = asyncio.Semaphore(10)
async def bounded(task):
async with semaphore:
return await task
async for task in task_generator:
asyncio.ensure_future(bounded(task))
此解决方案的问题是贪婪地从生成器中提取任务。例如,如果生成器从大型数据库中读取数据,则程序可能会耗尽内存。
除此之外,它是惯用法和行为规范的。
使用异步生成器协议来按需提取新任务的解决方案:
async def max10(task_generator):
tasks = set()
gen = task_generator.__aiter__()
try:
while True:
while len(tasks) < 10:
tasks.add(await gen.__anext__())
_done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
except StopAsyncIteration:
await asyncio.gather(*tasks)
它可能被认为不是最佳选择,因为它只有在10个可用时才开始执行任务。
这是使用工作者模式的简洁魔术解决方案:
async def max10(task_generator):
async def worker():
async for task in task_generator:
await task
await asyncio.gather(*[worker() for i in range(10)])
它依赖于某种违反直觉的特性,即能够在同一个异步生成器上具有多个异步迭代器,在这种情况下,每个生成的项只能由一个迭代器看到。
我的直觉告诉我,这些解决方案在cancellation上均无法正常运行。
答案 1 :(得分:0)
异步不是线程。例如,如果您的任务绑定了文件IO,则write them async using aiofiles
async with aiofiles.open('filename', mode='r') as f:
contents = await f.read()
然后用您的任务替换任务。如果您一次只想运行10个,请等待asyncio。每10个任务收集一次。
import asyncio
async def task(x):
await asyncio.sleep(0.5)
print( x, "is done" )
async def run(loop):
futs = []
for x in range(50):
futs.append( task(x) )
await asyncio.gather( *futs )
loop = asyncio.get_event_loop()
loop.run_until_complete( run(loop) )
loop.close()
如果您不能编写异步任务并需要线程,这是使用asyncio的ThreadPoolExecutor的基本示例。请注意,对于max_workers = 5,一次只能运行5个任务。
import time
from concurrent.futures import ThreadPoolExecutor
import asyncio
def blocking(x):
time.sleep(1)
print( x, "is done" )
async def run(loop):
futs = []
executor = ThreadPoolExecutor(max_workers=5)
for x in range(15):
future = loop.run_in_executor(executor, blocking, x)
futs.append( future )
await asyncio.sleep(4)
res = await asyncio.gather( *futs )
loop = asyncio.get_event_loop()
loop.run_until_complete( run(loop) )
loop.close()
答案 2 :(得分:0)
正如Dima Tismek所指出的那样,使用信号量来限制并发性很容易使task_generator
耗尽,因为在获取任务和将它们提交给事件循环之间没有背压。另一个答案也探讨了一个更好的选择,不是在生成器生成项目后立即生成任务,而是创建固定数量的同时使生成器用尽的工人。
可以在两个方面改进代码:
这是解决两个问题的实现:
async def throttle(task_generator, max_tasks):
it = task_generator.__aiter__()
cancelled = False
async def worker():
async for task in it:
try:
await task
except asyncio.CancelledError:
# If a generated task is canceled, let its worker
# proceed with other tasks - except if it's the
# outer coroutine that is cancelling us.
if cancelled:
raise
# other exceptions are propagated to the caller
worker_tasks = [asyncio.create_task(worker())
for i in range(max_tasks)]
try:
await asyncio.gather(*worker_tasks)
except:
# In case of exception in one worker, or in case we're
# being cancelled, cancel all workers and propagate the
# exception.
cancelled = True
for t in worker_tasks:
t.cancel()
raise
一个简单的测试用例:
async def mock_task(num):
print('running', num)
await asyncio.sleep(random.uniform(1, 5))
print('done', num)
async def mock_gen():
tnum = 0
while True:
await asyncio.sleep(.1 * random.random())
print('generating', tnum)
yield asyncio.create_task(mock_task(tnum))
tnum += 1
if __name__ == '__main__':
asyncio.run(throttle(mock_gen(), 3))