驱动程序编写中使用的许多异步类型都是线程不安全的, 他们会检查是否始终通过关联的同步 调度程序,以确保内存安全,例如:
{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([®]() {
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:解释了理论框架 同步检查背后的功能
- 线程安全的异步代码,其中提供了以下方面的指南: 通用生产代码,而不仅仅是测试。