简体   繁体   English

asyncio.gather 的顺序版本

[英]Sequential version of asyncio.gather

I tried to create a method similar to asyncio.gather, but which will execute the list of tasks sequentially and not asynchronously:我尝试创建一个类似于 asyncio.gather 的方法,但它将按顺序而不是异步执行任务列表:

async def in_sequence(*tasks):
    """Executes tasks in sequence"""
    for task in tasks:
        await task

Next this method was supposed to be used like this:接下来这个方法应该像这样使用:

async def some_work(work_name):
    """Do some work"""
    print(f"Start {work_name}")
    await asyncio.sleep(1)
    if raise_exception:
        raise RuntimeError(f"{work_name} raise an exception")
    print(f"Finish {work_name}")

async def main():
    try:
        await asyncio.gather(
            some_work("work1"),         # work1, work2, in_sequence and work5 executed in concurrently
            some_work("work2"),
            in_sequence(
                some_work("work3"),     # work3 and work4 executed in sequence
                some_work("work4")
            ),
            some_work("work5"),


    except RuntimeError as error:
        print(error)                    # raise an exception at any point to terminate

And everything worked fine until I tried to throw an exception in some_work:在我尝试在 some_work 中抛出异常之前一切正常:

async def main():
    try:
        await asyncio.gather(
            some_work("work1"),
            some_work("work2"),
            in_sequence(
                some_work("work3", raise_exception=True),       # raise an exception here
                some_work("work4")
            ),
            some_work("work5"),


    except RuntimeError as error:
        print(error)

Immediately after that, I received the following error message:紧接着,我收到以下错误消息:

RuntimeWarning: coroutine 'some_work' was never awaited

I read the documentation and continued to experiment:我阅读了文档并继续进行实验:

async def in_sequence(*tasks):
    """Executes tasks in sequence"""
    _tasks = []
    for task in tasks:
        _tasks.append(asyncio.create_task(task))

    for _task in _tasks:
        await _task

And this version worked as expected!这个版本按预期工作!

In this regard, I have next questions:在这方面,我有下一个问题:

  1. Why does the second version work and the first not?为什么第二个版本有效而第一个无效?
  2. Does asyncio already have the tools to execute the list of tasks sequentially? asyncio 是否已经拥有按顺序执行任务列表的工具?
  3. Have I chosen the right implementation method or are there better options?我选择了正确的实施方法还是有更好的选择?

You said that version of in_sequence works(with asyncio.create_task), but I think it does not.您说 in_sequence 的版本有效(使用 asyncio.create_task),但我认为它没有。 From docs来自文档

Wrap the coro coroutine into a Task and schedule its execution.将 coro 协程包装成一个 Task 并安排其执行。 Return the Task object.返回任务 object。

It seems that it runs coroutines in parallel, but you need them in sequence.它似乎并行运行协程,但您需要按顺序运行它们。

So experimented and found two ways how to fix this所以实验并找到了两种方法来解决这个问题

Use your original in_sequence function and add this code, that hides that error:使用您原来的 in_sequence function 并添加此代码,隐藏该错误:

import warnings
warnings.filterwarnings(
    'ignore',
    message=r'^coroutine .* was never awaited$',
    category=RuntimeWarning
)

Fix in_sequence function, like this:修复 in_sequence function,像这样:

async def in_sequence(*tasks):
    for index, task in enumerate(tasks):
        try:
            await task
        except Exception as e:
            for task in tasks[index + 1:]:
                task.close()
            raise e

Answers on other questions:其他问题的答案:

  1. That warnings is triggered by C++ code, when you do not have links on coroutine.当您在协程上没有链接时,该警告由 C++ 代码触发。 just simple code can show you this idea(in terminal):只需简单的代码就可以向您展示这个想法(在终端中):

async def test():
    return 1

f = test()
f = None # after that you will get that error
  1. I do not know我不知道
  2. See above看上面
  1. The first version doesn't work because in_sequence doesn't catch an exception which can be raised on await task .第一个版本不起作用,因为in_sequence没有捕获可以在await task上引发的异常。 The second works because create_task creates a future-like Task object that runs coroutine.第二个有效,因为create_task创建了一个运行协程的类似未来的任务object。 The object doesn't return/propagate a result of the wrapped coroutine. object 不会返回/传播包装协程的结果。 When you await the object, it suspends until either has a result or an exception set or until it is canceled .当您await object 时,它会挂起,直到有结果异常或直到它被取消

  2. It seems it hasn't.似乎没有。

  3. The second version will execute passed coroutines concurrently, so it is incorrect implementation.第二个版本会并发执行传递过来的协程,所以是不正确的实现。 If you really want to use some in_sequence function you can:如果你真的想使用一些in_sequence function 你可以:
    • Somehow delay the creation of coroutines.不知何故延迟了协程的创建。
    • Group sequential execution in an async functionasync function 中分组顺序执行

eg:例如:

async def in_sequence(*fn_and_args):
    for fn, args, kwargs in fn_and_args:
        await fn(*args, **kwargs)  # create a coro and await it in place

in_sequence(
    (some_work, ("work3",), {'raise_exception': True}),
    (some_work, ("work4",), {}),
)
async def in_sequence():
    await some_work("work3", raise_exception=True)
    await some_work("work4")

And this version worked as expected!这个版本按预期工作!

The problem with the second version is that it doesn't actually run the coroutines sequentially, it runs them in parallel.第二个版本的问题在于它实际上并没有顺序运行协程,而是并行运行它们。 This is because asyncio.create_task() schedules the coroutine to run in parallel with the current coroutines.这是因为asyncio.create_task()调度协程与当前协程并行运行。 So when you await tasks in a loop, you are actually allowing all the tasks to run while awaiting the first one.因此,当您在循环中等待任务时,实际上是在等待第一个任务时允许所有任务运行。 Despite appearances, the whole loop will run for only as long as the longest task.尽管看起来,整个循环只会运行最长的任务。 (See here for more details.) (有关详细信息,请参阅此处。)

The warning displayed by your first version is intended to prevent you from accidentally creating a coroutine that you never await, eg writing just asyncio.sleep(1) instead of await asyncio.sleep(1) .您的第一个版本显示的警告旨在防止您意外创建一个您从不等待的协程,例如只编写asyncio.sleep(1)而不是await asyncio.sleep(1) As far as asyncio is concerned, main is instantiating coroutine objects and passing them to in_sequence which "forgets" to await some of them.就 asyncio 而言, main正在实例化协程对象并将它们传递给“忘记”等待其中一些对象的in_sequence

One way to suppress the warning message is to allow the coroutine to spin, but cancel it immediately.抑制警告消息的一种方法是允许协程旋转,但立即取消它。 For example:例如:

async def in_sequence(*coros):
    remaining = iter(coros)
    for coro in remaining:
        try:
            await coro
        except Exception:
            for c in remaining:
                asyncio.create_task(c).cancel()
            raise

Note that a variable name that begins with an underscore marks an unused variable, so you shouldn't name variables so if you actually do use them.请注意,以下划线开头的变量名称标记未使用的变量,因此如果您确实使用它们,则不应命名变量。

Taking inspiration from user4815162342's and Anton Pomieshchenko's solutions, I came up with this variation of it:从 user4815162342 和 Anton Pomieshchenko 的解决方案中汲取灵感,我想出了它的这个变体:

async def in_sequence(*storm):
    twister = iter(storm)
    for task in twister:
        task = task() # if it's a regular function, it's done here.
        if inspect.isawaitable(task):
            try:
                await task # if it's also awaitable, await it
            except BaseException as e:
                task.throw(e) # if an error occurs, throw it into the coroutine
            finally:
                task.close() # to ensure coroutine closer

    assert not any(twister) # optionally verify that the iterator is now empty

this way you can combine regular functions with coroutines with this in_sequence .通过这种方式,您可以将常规函数与协程与此in_sequence结合起来。 But make sure to call it like this:但一定要这样称呼它:

await in_sequence(*[b.despawn, b.release])

Notice the lack of () ( __call__() ), because otherwise the regular function would be called immediately and the coroutine would throw a RuntimeWarning for having never been awaited.请注意缺少() ( __call__() ),因为否则常规的 function 将立即被调用,并且协程将因从未等待而引发RuntimeWarning ( b.despawn is a coroutine and b.release is not for my example) b.despawn是一个协程,而b.release不是我的例子)

You can also do an additional check for callable(task) just before invoking task() , but it's up to you.您还可以在调用task()之前对callable(task)进行额外检查,但这取决于您。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM