在编写驱动程序时,使用的许多异步类型都不是线程安全的,并且它们会检查是否始终从关联的同步调度程序使用,以确保内存安全,例如:
{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 服务器,并在主测试线程中使用测试 fixture 类。TestDispatcherBound
对象将成为测试 fixture 类的成员。
涉及 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
从主线程安全地与其交互。
请注意,我们已使用 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
中。这样,OutgoingDirectory
和 ServerBindingGroup
将从同一同步调度程序使用,并且您不会遇到任何崩溃问题。您可以查看使用此方法的示例测试。
一般来说,沿并发边界划分类会很有帮助。这样,您就可以确保需要在同一调度程序上使用的所有对象都已同步,从而防止潜在的崩溃或数据争用。
另请参阅
- 隔离状态异步 C++ RFC,其中介绍了同步检查背后的理论框架。
- 线程安全的异步代码,其中包含针对常规生产代码(而不仅仅是测试)的指导。