測試中的執行緒提示

在驅動程式庫編寫作業中使用的非同步類型,大多不安全,因此會檢查這些類型是否一律從相關聯的同步調度器使用,以確保記憶體安全,例如:

  • {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 中包裝執行緒不安全的非同步物件,確保所有對包裝物件的存取作業都會在相關聯的調度器上執行。

以下範例涉及 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);
}

在這個範例中,系統會在對應的 async::Loop 執行緒上存取 MyRegister 物件。這會反過來釋放主執行緒,以便進行阻斷式呼叫。當主執行緒想要與 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,才能安全地從主執行緒與其互動。

請注意,我們已將 SyncCall 包裝在 driver_runtime.PerformBlockingWork 中。這會在主執行緒上執行前景驅動程式庫調度器,同時在背景中的新暫時執行緒上執行 SyncCall。如果在調度器繫結物件 (在本例中為 GetValue) 上執行的方法涉及與前景調度器相關聯的物件 (在本例中為 BlockingIO),就必須執行前景調度器。

如果您確定呼叫的方法不需要在前景調度器執行時才能傳回,那麼可以使用直接 SyncCall

DispatcherBound 物件的細緻程度

以下指南適用於 TestDispatcherBound 和其實際執行環境對應項目 DispatcherBound

當您要透過特定同步調度器序列化物件的存取權時,請務必考量從該調度器使用哪些其他物件。在同一個 DispatcherBound 中結合兩個物件可能會更有效率。

舉例來說,如果您使用 component::OutgoingDirectory,並以同步方式呼叫 FIDL 伺服器實作項目 (例如將繫結項目新增至 fidl::ServerBindingGroup),則必須確保兩個物件位於相同的調度器上。

如果您只將 OutgoingDirectory 放入 [Test]DispatcherBound 中,以便處理其同步檢查器,但將 ServerBindingGroup 留在其他位置 (例如主執行緒),當 OutgoingDirectory 物件從調度器執行緒呼叫 ServerBindingGroup 時,就會發生當機,並觸發 ServerBindingGroup 中的檢查器。

如要解決這個問題,您可以將 OutgoingDirectory 和其參照的物件 (例如 ServerBindingGroup 或任何伺服器狀態) 放入較大的物件中,然後將該物件放入 DispatcherBound。這樣一來,OutgoingDirectoryServerBindingGroup 都會從同一個同步調度器使用,您就不會遇到任何當機情形。您可以查看使用這項技巧的測試範例

一般來說,按照並行作業邊界劃分類別會很有幫助。這樣一來,您就能確保所有需要在同一個調度器上使用的物件都會同步處理,避免發生可能的當機或資料競爭情形。

另請參閱