驅動程式庫寫入中所使用的一些非同步類型都屬於不安全的執行緒,且會「檢查」是否一律透過相關聯的同步調度工具使用這些類型,以確保記憶體安全,例如:
{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);
}
在本例中,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([®]() {
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
中。這樣一來,OutgoingDirectory
和 ServerBindingGroup
就會從同一個同步處理的調度工具使用,不會發生任何當機情形。您可以看到使用這項技巧的範例測試。
一般來說,按照並行界線劃分類別會很有幫助。如此可確保所有需要在同一調派程式上使用的物件皆已同步處理,避免發生潛在的當機問題或資料競爭。
另請參閱
- 隔離狀態非同步 C++ RFC,其中說明同步處理檢查背後的理論架構。
- 執行緒安全非同步程式碼:提供一般實際工作環境程式碼 (不只是測試) 的指引。