测试中的线程处理技巧

驱动程序编写中使用的许多异步类型都是线程不安全的, 他们会检查是否始终通过关联的同步 调度程序,以确保内存安全,例如:

  • {fdf,component}::OutgoingDirectory
  • {fdf,fidl}::Client{fdf,fidl}::WireClient
  • {fdf,fidl}::ServerBinding{fdf,fidl}::ServerBindingGroup

如果您要测试包含这些类型的驱动程序异步对象,请在 错误的执行上下文会导致崩溃,其中堆栈轨迹 “synchronization_checker”。这是一项安全功能,可防止数据静默 或破坏。以下是一些有助于避免崩溃的提示。

编写单线程测试

最简单的方法是执行测试断言并使用 这些对象来自同一线程(通常是主线程):

涉及 async::Loop 的示例:

TEST(MyRegister, Read) {
  async::Loop loop{&kAsyncLoopConfigAttachToCurrentThread};
  // For illustration purposes, this is the thread-unsafe type being tested.
  MyRegister reg{loop.dispatcher()};
  // Use the object on the current thread.
  reg.SetValue(123);
  // Run any async work scheduled by the object, also on the current thread.
  ASSERT_OK(loop.RunUntilIdle());
  // Read from the object on the current thread.
  EXPECT_EQ(obj.GetValue(), 123);
}

涉及 fdf_testing::DriverRuntime 的前台调度程序的示例:

TEST(MyRegister, Read) {
  // Creates a foreground driver dispatcher.
  fdf_testing::DriverRuntime driver_runtime;
  // For illustration purposes, this is the thread-unsafe type being tested.
  MyRegister reg{dispatcher.dispatcher()};
  // Use the object on the current thread.
  reg.SetValue(123);
  // Run any async work scheduled by the object, also on the current thread.
  driver_runtime.RunUntilIdle();
  // Read from the object on the current thread.
  EXPECT_EQ(obj.GetValue(), 123);
  ASSERT_OK(dispatcher.Stop());
}

请注意,fdf_testing::DriverRuntime 还可以创建后台驱动程序 由驱动程序运行时的代管式线程池驱动的调度程序。 这通过 StartBackgroundDispatcher 方法完成。 与这些后台驱动程序调度程序关联的线程不安全对象 不得直接从主线程访问

在单个线程中使用异步对象时,这些对象包含的 async::synchronization_checker不会惊慌。

调用屏蔽函数

如果您需要调用阻塞函数,那么单线程方式会出现问题 它会阻止调度程序处理某些消息。如果您首先调用 阻塞函数,然后运行循环,这将死锁,因为 在屏蔽函数返回结果且该屏蔽函数 不会返回任何值。

要调用阻塞函数,您需要一种方法来运行该函数并运行循环。 不同线程上此外,屏蔽函数不得直接 在不同步的情况下访问与调度程序关联的对象, 因为这可能会与循环线程争用。

要解决这两个问题,您可以将线程不安全的异步对象封装在 async_patterns::TestDispatcherBound,这可确保 对所封装对象的所有访问都发生在其关联的调度程序上。

重复使用了之前的 MyRegister 类型涉及 async::Loop 的示例:

// Let's say this function blocks and then returns some value we need.
int GetValueInABlockingWay();

TEST(MyRegister, Read) {
  // Configure the loop to register itself as the dispatcher for the
  // loop thread, such that the |MyRegister| constructor may use
  // `async_get_default_dispatcher()` to obtain the loop dispatcher.
  async::Loop loop{&kAsyncLoopConfigNoAttachToCurrentThread};
  loop.StartThread();

  // Construct the |MyRegister| on the loop thread.
  async_patterns::TestDispatcherBound<MyRegister> reg{
      loop.dispatcher(), std::in_place};

  // Schedule a |SetValue| call on the loop thread and wait for it.
  reg.SyncCall(&MyRegister::SetValue, 123);

  // Call the blocking function on the main thread.
  // This will not deadlock, because we have started a loop thread
  // earlier to process messages for the |MyRegister| object.
  int value = GetValueInABlockingWay();
  EXPECT_EQ(value, 123);

  // |GetValue| returns a value; |SyncCall| will proxy that back.
  EXPECT_EQ(reg.SyncCall(&MyRegister::GetValue), 123);
}

在此示例中,对 MyRegister 对象的访问发生在其 async::Loop 个会话。进而释放主线程进行阻塞调用。 当主线程想要与 MyRegister 交互时,需要这样做 间接使用 SyncCall

测试中的另一种常见模式是在单独的线程上设置 FIDL 服务器 TestDispatcherBound,并在主函数上使用测试固件类 测试线程。TestDispatcherBound 对象将成为测试成员 定义。

涉及 fdf_testing::DriverRuntime 的示例:

在驱动程序中,阻塞工作本身通常发生在驱动程序内部。例如: 阻塞工作可能涉及驱动程序通过 测试期间伪造的 FIDL 协议。在以下示例中, BlockingIO 类表示驱动程序,FakeRegister 类表示 表示 BlockingIO 使用的某些 FIDL 协议的虚构实现。

// Here is the bare skeleton of a driver object that makes a synchronous call.
class BlockingIO {
 public:
  BlockingIO(): dispatcher_(fdf_dispatcher_get_current_dispatcher()) {}

  // Let's say this function blocks to update the value stored in a
  // |FakeRegister|.
  void SetValueInABlockingWay(int value);

  /* Other details omitted */
};

TEST(BlockingIO, Read) {
  // Creates a foreground driver dispatcher.
  fdf_testing::DriverRuntime driver_runtime;

  // Create a background dispatcher for the |FakeRegister|.
  // This way it is safe to call into it synchronously from the |BlockingIO|.
  fdf::UnownedSynchronizedDispatcher register_dispatcher =
      driver_runtime.StartBackgroundDispatcher();

  // Construct the |FakeRegister| on the background dispatcher.
  // The |FakeRegister| constructor may use
  // `fdf_dispatcher_get_current_dispatcher()` to obtain the dispatcher.
  async_patterns::TestDispatcherBound<FakeRegister> reg{
      register_dispatcher.async_dispatcher(), std::in_place};

  // Construct the |BlockingIO| on the foreground driver dispatcher.
  BlockingIO io;

  // Call the blocking function. The |register_dispatcher| will respond to it in the
  // background.
  io.SetValueInABlockingWay(123);

  // Check the value from the fake.
  // |GetValue| returns an |int|; |SyncCall| will proxy that back.
  // |PerformBlockingWork| will ensure the foreground dispatcher is running while
  // the blocking work runs in a new temporary background thread.
  EXPECT_EQ(driver_runtime.PerformBlockingWork([&reg]() {
    return reg.SyncCall(&FakeRegister::GetValue);
  }), 123);
}

当驱动程序对象的调度程序由主线程提供支持时,有 无需通过TestDispatcherBound。我们可以安全地使用 BlockingIO 驱动程序对象,包括进行 SetValueInABlockingWay 调用; 。

FakeRegister 虚构对象位于 register_dispatcher 上时,我们需要 使用 TestDispatcherBound 从主线程安全地与其交互。

请注意,我们使用 driver_runtime.PerformBlockingWork 封装了 SyncCall。 这对我们来说就是在主线程上运行前台驱动程序调度程序, 同时在后台的新临时线程上运行 SyncCall。 如果调用在 与调度程序绑定的对象(在本例中为 GetValue),涉及与对象对话 与前台调度程序相关联,此处为 BlockingIO

如果确定调用的方法不需要前台调度程序 则直接 SyncCall 可以使用。

DispatcherBound 对象的粒度

以下指南适用于 TestDispatcherBound 及其正式版 副本 DispatcherBound

当通过序列化特定同步实例发生对对象的访问时, 因此,有必要考虑要使用哪些其他对象, 同一个调度程序。将两个对象组合起来会更有效, 相同的 DispatcherBound

例如,如果您使用的是 component::OutgoingDirectory, 同步调用 FIDL 服务器实现,例如将绑定添加到 fidl::ServerBindingGroup,则必须确保两个对象位于同一位置 调度程序。

如果您只将 OutgoingDirectory 放在 [Test]DispatcherBound 中才能正常运行 但需要将 ServerBindingGroup 保留在某个位置 其他,例如在主线程上,当 OutgoingDirectory 发生时,您会遇到崩溃。 对象从调度程序线程到 ServerBindingGroup 的调用,触发 ServerBindingGroup 中的检查工具。

如需解决此问题,您可以将 OutgoingDirectory 及其对象 例如 ServerBindingGroup 或任何服务器状态, 然后将该对象放入 DispatcherBound 中。这样一来, OutgoingDirectoryServerBindingGroup 将自 调度程序,就不会再遇到任何崩溃。您可以 采用此技术的测试示例

一般来说,最好沿着并发边界划分类。修改者 这样做可以确保需要在同一位置使用的所有对象 调度程序会保持同步,以防止潜在的崩溃或数据争用。

另请参阅