在 Fuchsia Controller 中使用异步 Python 的最佳实践

本页面尽可能介绍了异步 Python 的常见任务流, 使用异步 Python 时会遇到的一些问题。

继续操作之前,请查看此 官方文档,关于 asyncio 和 Fuchsia 控制器教程

背景

以下各项通常与异步代码相关,不一定是特定的 至 Python:

  • 异步代码不一定是多线程的。如果 与 Fuchsia 控制器相关的代码,几乎所有内容都是单线程的。
  • 异步代码在循环(也称为执行器)内运行, 将释放到相应的优先级

因此,这可能会使某些问题难以调试,例如 如果某个后台任务打算永久运行,但又引发了 异常。如果您从不等待此任务, 日志。

常见误区

未保留对任务的引用

如果您在循环中启动任务,例如:

# Don't do this!
asyncio.get_running_loop().create_task(foo_bar())

而您没有保存对返回值的引用,则有可能 Python 垃圾回收器可能会随时取消和删除此任务 。

请始终确保将创建的任务存储在变量中,例如:

# Always store a variable!
foo_bar_task = asyncio.get_running_loop().create_task(foo_bar())

然后,确保任务的生命周期与需要的内容相关。如果这是 特定测试用例,请将任务存储为测试用例类的成员, 在拆解期间取消它

将阻塞代码与异步代码混用

在测试代码时,某些实验通常会抛出 sleep 语句。切勿将 asyncio.sleeptime.sleep。前者返回一个协程,而后者会阻止整个 循环。

例如:

import asyncio
import time


async def printer():
    """ Prints a thing."""
    print("HELLO")


async def main():
    task = asyncio.get_running_loop().create_task(printer())
     time.sleep(1) # THIS WILL BLOCK EXECUTION. 
    task.cancel()

if __name__ == "__main__":
    asyncio.run(main())

上面的代码不会输出任何内容。但是,如果您输入 HELLO, 将 time.sleep(1) 替换为 await asyncio.sleep(1),从而得到 异步循环。

FIDL 服务器错误

鉴于异步编程的性质,您可以使用 您的 FIDL 服务器实现中存在的错误而不知道,特别是 FIDL 服务器实现已连接到 Fuchsia 设备上的组件。

遗憾的是,这种情况难以调试,而且只能以 由于 FIDL 服务器实现崩溃而导致设备上的 PEER_CLOSED 错误。

一种方法(用于调试)是向任务添加一个回调,用于检查 任务中发生了异常,并向日志发送一些内容( 使程序崩溃)。

例如:

def log_exception(task):
    try:
        _ = task.result()
    except Exception as e:
        # Can log the exception here for debugging.

task = asyncio.get_running_loop().create_task(foobar())
task.add_done_callback(log_exception)

常见任务流

同步任务

建议您先查看 有关入门指南的 Python 文档页面 可用的同步对象。请注意,这些对象并非线程安全对象。 然而,其中一些示例值得参考。

正在等待多个任务

您可能需要等待多个任务完成。为此, 使用 asyncio.wait。想做点什么 类似于 select 系统调用,请将 return_when 参数设为 asyncio.FIRST_COMPLETED,例如:

done, pending = await asyncio.wait(
    [task1, task2, task3],
    return_when=asyncio.FIRST_COMPLETED
)

结果包含一个任务元组(在本例中为 donepending)。哪个 可以像对待任何其他任务对象一样处理。对于 done 对象,可以执行以下操作: 遍历这些指标,并通过检查 result() 函数来收集结果。

在同步代码中运行异步代码

在编写本文时,大部分 Fuchsia 控制器代码都是从 同步代码。为确保任务可以在后台运行,合理的做法是 通过 asyncio.new_event_loop() 保留一个循环实例。

原因在于 asyncio 任务必须保持在单个循环中。这个 asyncio.Queue 等同步对象也是如此。

如果您交互的代码足够简单(运行单个 FIDL) 方法),则可以使用:

asyncio.run_until_complete(...)

但是,如果您需要运行任务或处理同步, 将内容封装在包含循环的类中。请务必确保 都有用于关闭循环的拆解代码, 例如:

class FidlHandlingClass():

    def __init__(self):
        self._loop = asyncio.new_event_loop()
        # Can launch tasks here.
        self._important_task = self._loop.create_task(foo())

    def __del__(self):
        self._important_task.close()
        self._loop.close()

您还可以使用装饰器扩展类,使循环隐式地 用于所有公共函数中(尽管它们对调用方而言会显示为同步) 例如:

from functools import wraps

class FidlInstance():

    def __init__(self, proxy_channel):
        self._loop = asyncio.new_event_loop()
        self.echo_proxy = fidl.fuchsia_developer_ffx.Echo.Client(proxy_channel)

    def __del__(self):
        self._loop.close()

    def _asyncmethod(f):
        @wraps(f)
        def wrapped(inst, *args, **kwargs):
            coro = f(inst, *args, **kwargs)
            inst._loop.run_until_complete(coro)
        return wrapped

    @_asyncmethod
    async def echo(self, string: str):
        return await self.echo_proxy.echo_string(value=string)

然后,可以使用以下代码调用上述类:

client_channel = ...
inst = FidlInstance(client_channel)
print(inst.echo("foobar"))

虽然此实例可在非异步上下文中运行,但它仍在运行 异步代码。不过,这样会大大降低总体读写操作难度。

调整代码以支持异步 Python

FIDL 是一种围绕异步设计的语言。不过,像下面这样测试框架: Mobly 需要同步的 Python 函数来运行测试。如果您有 主要用于同步测试和 Python,那么这适用于您。

fuchsia-controllerwrappers 模块下包含一些 mixin 代码, 支持在同步上下文(特别是 AsyncAdapter)中使用异步代码, 它可用作封装容器或 mixin,而装饰器 asyncmethod

当彼此一起使用时,这会创建一个设置,其中所有代码 在给定实例中运行的测试针对常见的 asyncio 事件循环执行。

在大多数情况下,您可以使用 共 asyncio.run() 页。然而,您可能需要等待队列或异步任务 检查状态,这变得非常难以 用途。每次调用 asyncio.run() 都会创建一个全新的事件循环, 异步数据结构的管理可能会带来麻烦, 一次只能用于一个事件循环中。保留单个 asyncio 事件 您可以避免遇到这些异常。

例如,假设您要编写一个 Mobly 测试用例,但要构建的类 公开依赖于某种底层异步 Python,您可以像这样 因此可以在 Mobly 中使用:

import asyncio
from mobly import asserts, base_test, test_runner
from fuchsia_controller_py.wrappers import AsyncAdapter, asyncmethod

# AsyncAdapter should be included first in the list of base classes.
class ClassYouWantToTest(AsyncAdapter, SomeBaseClass):

    def __init__(self):
        super().__init__()
        self.async_init()

    @asyncmethod
    async def async_init(self):
        self.queue: asyncio.Queue[int] = asyncio.Queue()
        await self.queue.put(1)

    @asyncmethod
    async def function_we_care_about(self) -> int:
        got = await self.queue.get()
        self.queue.task_done()
        return got


class ExampleTest(base_test.BaseTestClass):

    def test_case_example(self) -> None:
        """Example... doesn't really do much useful."""
        c = ClassYouWantToTest()
        asserts.assert_equal(c.function_we_care_about(), 1)

if __name__ == "__main__":
    test_runner.main()

方法 function_we_care_about 将被封装,以便在功能上 执行 Mobly 测试用例时,系统会执行以下代码:

def function_we_care_about(self) -> None:
  coro = self._function_we_care_about_impl()
  self._mixin_asyncio_loop.run_until_complete(coro)