异步处理批处理任务

时间:2019-03-03 08:53:52

标签: python-3.x concurrency python-asyncio

我有一个生成任务的函数(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()

有人可以帮助我吗?我不知道在哪里放置异步/等待关键字

3 个答案:

答案 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))