什么时候是调用loop.close()的合适时间?

时间:2018-05-10 18:41:33

标签: python python-asyncio

我已经尝试了asyncio一段时间并阅读了PEPs;一些教程;甚至是O'Reilly book

我认为我掌握了它,但我仍然对loop.close()的行为感到困惑,我无法理解它是什么时候安全"调用。

蒸馏到最简单,我的用例是一堆阻塞"旧学校"我在run_in_executor()和外部协程中包含的调用;如果这些调用中的任何一个出错了,我想停止进度,取消那些仍然未完成的,打印一个合理的日志,然后(希望,干净地)让它们走开。

说,像这样:

import asyncio
import time


def blocking(num):
    time.sleep(num)
    if num == 2:
        raise ValueError("don't like 2")
    return num


async def my_coro(loop, num):
    try:
        result = await loop.run_in_executor(None, blocking, num)
        print(f"Coro {num} done")
        return result
    except asyncio.CancelledError:
        # Do some cleanup here.
        print(f"man, I was canceled: {num}")


def main():
    loop = asyncio.get_event_loop()
    tasks = []
    for num in range(5):
        tasks.append(loop.create_task(my_coro(loop, num)))

    try:
        # No point in waiting; if any of the tasks go wrong, I
        # just want to abandon everything. The ALL_DONE is not
        # a good solution here.
        future = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
        done, pending = loop.run_until_complete(future)
        if pending:
            print(f"Still {len(pending)} tasks pending")
            # I tried putting a stop() - with/without a run_forever()
            # after the for - same exception raised.
            #  loop.stop()
            for future in pending:
                future.cancel()

        for task in done:
            res = task.result()
            print("Task returned", res)
    except ValueError as error:
        print("Outer except --", error)
    finally:
        # I also tried placing the run_forever() here,
        # before the stop() - no dice.
        loop.stop()
        if pending:
            print("Waiting for pending futures to finish...")
            loop.run_forever()
        loop.close()

我尝试了stop()run_forever()调用的几种变种,首先是" run_forever,然后停止"似乎是根据to the pydoc使用的那个,并且在没有调用close()的情况下产生令人满意的结果:

Coro 0 done
Coro 1 done
Still 2 tasks pending
Task returned 1
Task returned 0
Outer except -- don't like 2
Waiting for pending futures to finish...
man, I was canceled: 4
man, I was canceled: 3

Process finished with exit code 0

但是,当添加对close()的调用时(如上所示),我有两个例外:

exception calling callback for <Future at 0x104f21438 state=finished returned int>
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/futures.py", line 414, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 620, in call_soon_threadsafe
    self._check_closed()
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

这对我来说最烦人,但对我来说,完全令人费解:而且,更糟糕的是,我一直无法弄清楚处理这种情况的正确方法是什么。

因此,有两个问题:

  • 我错过了什么?我应该如何修改上面的代码,使得包含close()的调用不会引发?

  • 如果我不打电话给close()会发生什么事实 - 在这个微不足道的情况下,我认为它很大程度上是多余的;但是真正的&#34;会带来什么后果呢?生产代码?

为了我个人的满意,还有:

  • 为什么会提高?循环还需要更多来自coros / tasks:它们要么退出;提高;或被取消:这还不足以让它快乐吗?

非常感谢您提出任何建议!

2 个答案:

答案 0 :(得分:2)

  

蒸馏到最简单,我的用例是一堆阻塞&#34;旧学校&#34;我在run_in_executor()和外部协程中包含的调用;如果这些电话中的任何一个出错,我想停止进度,取消尚未完成的电话

这不能像预想的那样工作,因为run_in_executor将函数提交给线程池,并且不能在Python(或暴露它们的其他语言)中取消OS线程。取消run_in_executor返回的未来将尝试取消基础concurrent.futures.Future,但只有在阻止功能尚未运行时才会生效,例如因为线程池很忙。一旦开始执行,就无法安全取消。支持安全可靠的取消是使用asyncio与线程相比的好处之一。

如果您正在处理同步代码,无论是遗留阻塞调用还是长时间运行的CPU绑定代码,您应该使用run_in_executor运行它,并采用一种方法来中断它。例如,代码偶尔可以检查stop_requested标志并退出,如果这是真的,可能是通过引发异常。然后你就可以取消&#34;通过设置适当的标志或标志来完成这些任务。

  

如何修改上面的代码,方法是调用close()包括不引发?

据我所知,如果不修改blocking和顶级代码,目前无法这样做。 run_in_executor将坚持告知结果的事件循环,并且在事件循环关闭时失败。取消asyncio future是没有用的,因为取消检查是在事件循环线程中执行的,并且在工作线程调用call_soon_threadsafe之前发生错误。 (可能会将检查移至工作线程,但应仔细分析是否在调用cancel()和实际检查之间导致竞争条件。)

  

为什么会提高?循环还需要更多来自coros / tasks:它们要么退出;提高;或被取消:这还不足以让它快乐吗?

它希望阻塞函数传递给run_in_executor(在问题中字面上称为blocking),它们已经在事件循环关闭之前已经开始完成运行。您取消了asyncio的未来,但潜在的并发未来仍然希望&#34;打电话回家&#34;,发现循环已关闭。

这是不是asyncio中的错误,或者您是否应该关闭事件循环,直到您以某种方式确保提交到run_in_executor的所有工作都完成后,这一点并不明显。这样做需要进行以下更改:

  • 不要尝试取消未决期货。取消它们在表面上看起来是正确的,但是它会阻止你为这些未来wait(),因为asyncio会认为它们已经完成。
  • 相反,请向您的后台任务发送特定于应用程序的事件,通知他们需要中止。
  • loop.run_until_complete(asyncio.wait(pending))之前致电loop.close()

通过这些修改(特定于应用程序的事件除外 - 我只是让sleep()完成他们的课程),没有出现异常。

  

如果我不打电话给close(),实际会发生什么 - 在这个微不足道的案例中,我认为它很大程度上是多余的;但是真正的&#34;会带来什么后果呢?生产代码?

由于典型的事件循环与应用程序一样长,因此在程序的最后不要调用close()时应该没有问题。无论如何,操作系统将清除程序退出时的资源。

调用loop.close()对于具有明确生命周期的事件循环非常重要。例如,库可能会为特定任务创建一个新的事件循环,在专用线程中运行它并处置它。未能关闭此循环可能会泄漏其内部资源(例如用于线程间唤醒的管道)并导致程序失败。另一个例子是测试套件,它通常为每个单元测试启动一个新的事件循环,以确保测试环境的分离。

<小时/> 编辑:filed a bug就此问题而言 编辑2:开发人员的错误是fixed

答案 1 :(得分:0)

在修复upstream issue之前,另一种解决问题的方法是将run_in_executor的使用替换为没有缺陷的自定义版本。虽然首先将自己的run_in_executor视为一个糟糕的主意,但事实上它只是concurrent.futuresasyncio未来之间的一个小粘合剂。

可以使用这两个类的公共API干净地实现run_in_executor的简单版本:

def run_in_executor(executor, fn, *args):
    """Submit FN to EXECUTOR and return an asyncio future."""
    loop = asyncio.get_event_loop()
    if args:
        fn = functools.partial(fn, *args)
    work_future = executor.submit(fn)
    aio_future = loop.create_future()
    aio_cancelled = False

    def work_done(_f):
        if not aio_cancelled:
            loop.call_soon_threadsafe(set_result)

    def check_cancel(_f):
        nonlocal aio_cancelled
        if aio_future.cancelled():
            work_future.cancel()
            aio_cancelled = True

    def set_result():
        if work_future.cancelled():
            aio_future.cancel()
        elif work_future.exception() is not None:
            aio_future.set_exception(work_future.exception())
        else:
            aio_future.set_result(work_future.result())

    work_future.add_done_callback(work_done)
    aio_future.add_done_callback(check_cancel)

    return aio_future

loop.run_in_executor(blocking)替换为run_in_executor(executor, blocking)executormain()中创建的ThreadPoolExecutor时,代码无需其他修改即可运行。

当然,在这个变体中,同步函数将继续在另一个线程中运行,尽管被取消 - 但是如果不修改它们以支持显式中断,这是不可避免的。