测试中的线程处理技巧

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

  • {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 不会 panic。

来电屏蔽函数

如果您需要调用阻塞函数,而该函数会阻止调度程序处理某些消息,则单线程方式会发生故障。如果您先调用阻塞函数,然后再运行循环,就会出现死锁情况,因为在阻塞函数返回之前循环不会运行,而且阻塞函数只有在循环运行后才会返回。

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

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

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

// 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 间接进行交互。

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

涉及 fdf_testing::DriverRuntime 的示例:

在驱动程序中,阻塞工作本身通常发生在驱动程序内部。例如,阻塞工作可能涉及驱动程序通过在测试期间伪造的 FIDL 协议进行的同步 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 都将通过同一个已同步调度程序使用,并且您不会遇到任何崩溃问题。您可以看到使用此方法的测试示例

一般来说,沿着并发边界划分类会很有帮助。这样做可以确保需要在同一调度程序上使用的所有对象保持同步,从而防止潜在的崩溃或数据争用。

另请参阅