新的 C++ 绑定可适应各种线程处理模型。根据应用的架构,您可以选择不同的类和使用方式。本文档介绍了在非平凡的线程环境中如何使用 FIDL 的工具和技巧。
背景:FIDL 连接的生命周期
在 FIDL 连接的生命周期内,从线程安全和防止释放后使用 (use-after-free) 的角度来看,以下情况非常重要:
到绑定调用:这些是用户代码在 FIDL 消息传递对象上进行的调用,即从 FIDL 运行时的角度来看是入站调用。例如:
- 在客户端上进行 FIDL 方法调用是绑定调用。
- 使用完成器从服务器实现中创建回复也是一个绑定调用。
面向用户的调用:这些是由 FIDL 运行时在用户对象(包括用户提供的回调)上进行的调用,即从 FIDL 运行时的角度来看是出站调用。例如:
- 在服务器实现上调用 FIDL 方法处理程序的服务器消息调度程序是面向用户的调用。
- 通过回调向用户传递双向 FIDL 方法的响应的 FIDL 客户端也是一个“向用户”的调用。
- 错误处理程序也是面向用户的调用。
从绑定的角度来看,用户对象比 FIDL 绑定高一层,因此面向用户的调用有时也称为“上调”。
拆解:停止消息分派的操作。具体而言,当拆解完成时,绑定将不再进行任何面向用户的调用;面向绑定的调用将失败或产生无效/微不足道的效果。示例:
- 调度期间发生的错误。
- 销毁
{fidl,fdf}::[Wire]Client。 - 正在呼叫
{fidl,fdf}::[Wire]SharedClient::AsyncTeardown()。
拆解通常会导致客户端/服务器端点关闭。
解除绑定:停止消息分派的操作,此外还会恢复用于发送和接收消息的客户端/服务器端点。这样做必然涉及拆解。示例:
- 正在呼叫
fidl::ServerBindingRef::Unbind()。
- 正在呼叫
拆解期间的释放后使用风险
销毁一组相关对象(包括 FIDL 客户端或服务器)时,必须注意它们的销毁顺序,以确保 FIDL 绑定运行时进行的 to-user 调用不会最终调用到已销毁的对象。
举个具体示例,假设 MyDevice 对象拥有一个 FIDL 客户端,并进行多次双向 FIDL 调用,每次都传递一个捕获 this 的 lambda 作为结果回调。在客户端可能仍在调度消息的同时销毁 MyDevice 是不安全的。当用户从非调度器线程(即未监控和调度当前 FIDL 绑定的消息的线程)销毁 MyDevice(或其他业务对象)时,通常会发生这种情况。
在销毁时处理事件以及处理来自服务器的方法调用时,也存在类似的释放后使用风险。
此问题有多种解决方案,但都旨在在用户对象的销毁与 to-user 调用之间添加互斥:
- 调度:确保相关用户对象的销毁永远不会与任何面向用户的调用并行调度。
- 引用计数:对用户对象进行引用计数,以便在绑定拆除完成之前不会销毁这些对象。
- 两阶段关闭:在绑定拆解完成后提供通知,以便用户安排业务对象在此之后销毁。
C++ 绑定原生支持上述所有方法。在某些情况下,引用计数并不适用,因此在使用绑定时,它是一种选择加入的功能。
客户端线程处理
有两种客户端类型支持异步操作:fidl::Client 和 fidl::SharedClient。如需详细了解其语义,请参阅客户端标头中的相关文档。
fidl::Client 的线程处理行为也适用于 fidl::WireClient。
同样,fidl::SharedClient 的线程处理行为也适用于 fidl::WireSharedClient。
客户端
fidl::Client 通过强制要求从以下位置使用 解决方案 1(调度)来支持该解决方案:
- 您只能从在该调度器上运行的任务中进行 FIDL 方法调用。
- 客户端对象本身无法移至另一个对象,然后从在其他调度程序上运行的任务中销毁。
这可确保在调度 FIDL 消息或错误事件时,不会销毁包含的用户对象。它适用于单线程和面向对象的用法。
fidl::Client 只能与同步异步调度程序搭配使用。async::Loop 的一种特殊用法是通过 loop.StartThread() 创建单个工作器线程,并通过 loop.Shutdown() 从其他线程联接该线程并关闭循环。从技术上讲,这里涉及两个线程,但从互斥访问的角度来看,这是安全的,并且 fidl::Client 旨在允许这种用法。
fidl::Client 通过事件处理脚本的 on_fidl_error 虚拟方法报告错误。用户发起的拆解(例如通过销毁客户端)不会作为错误报告给事件处理脚本。
fidl::Client 不拥有该事件处理脚本。相反,拥有客户端的用户对象可以实现事件处理接口,并将借用的指针传递给客户端对象。
fidl::Client 的典型用法可能如下所示:
class MyDevice : fidl::AsyncEventHandler<MyProtocol> {
public:
MyDevice() {
client_.Bind(std::move(client_end), dispatcher, /* event_handler */ this);
}
void on_fidl_error(fidl::UnbindInfo error) {
// Handle errors...
}
void DoThing() {
// Capture |this| such that the |MyDevice| instance may be accessed
// in the callback. This is safe because destroying |client_| silently
// discards all pending callbacks registered through |Then|.
client_->Foo(args).Then([this] (fidl::Result<Foo>&) { ... });
}
private:
fidl::Client<MyProtocol> client_;
};
请注意,当 MyDevice 被销毁时,不需要任何特殊操作 - 客户端绑定将作为进程的一部分被拆除,并且 fidl::Client 执行的线程检查足以防止此类释放后使用。
ThenExactlyOnce 带来的其他释放后使用风险
当客户端对象被销毁时,通过 ThenExactlyOnce 注册的待处理回调将异步接收到取消错误。需要注意确保所有 lambda 捕获仍处于有效状态。例如,如果某个对象包含 fidl::Client 并在异步方法回调中捕获 this,那么在销毁该对象后,在回调中操纵捕获的 this 将导致 use-after-free。为避免这种情况,请使用 Then 在接收器对象与客户端一起销毁时注册回调。以MyDevice为例:
void MyDevice::DoOtherThing() {
// Incorrect:
client_->Foo(request).ThenExactlyOnce([this] (fidl::Result<Foo>& result) {
// If |MyDevice| is destroyed, this pending callback will still run.
// The captured |this| pointer will be invalid.
});
// Correct:
client_->Foo(request).Then([this] (fidl::Result<Foo>& result) {
// The callback is silently dropped if |client_| is destroyed.
});
}
当回调捕获需要仅使用一次的对象时,可以使用 ThenExactlyOnce,例如在传播用作满足服务器请求一部分的客户端调用的错误时:
class MyServer : public fidl::Server<FooProtocol> {
public:
void FooMethod(FooMethodRequest& request, FooMethodCompleter::Sync& completer) override {
bar_.client->Bar().ThenExactlyOnce(
[completer = completer.ToAsync()] (fidl::Result<Bar>& result) {
if (!result.is_ok()) {
completer.Reply(result.error_value().status());
return;
}
// ... more processing
});
}
private:
struct BarManager {
fidl::Client<BarProtocol> client;
/* Other internal state... */
};
std::unique_ptr<BarManager> bar_;
};
在上述示例中,如果服务器想要重新初始化 bar_,同时保持 FooProtocol 连接处于活动状态,则可以在处理 FooMethod 时使用 ThenExactlyOnce 回复取消错误,或引入重试逻辑。
SharedClient
fidl::SharedClient 支持解决方案 2(引用计数)和解决方案 3(两阶段关机)。您可以从任意线程对 SharedClient 进行 FIDL 调用,并从任何类型的异步调度器使用共享客户端。与 Client 不同,销毁客户端可立即保证不再有 to-user 调用,而销毁 SharedClient 仅会启动异步绑定拆除。用户可能会异步观察到拆解完成。这样一来,便可将 SharedClient 移动或克隆到调度程序线程以外的其他线程,并在存在并行 to-user 调用(例如响应回调)时销毁/调用客户端的清理。这两个操作会竞争(如果客户端过早销毁,可能会取消响应回调),但 SharedClient 在通知其拆解完成之后,将不再进行任何面向用户的调用。
您可以通过以下两种方式来观察拆解完成情况:
自有事件处理脚本
在绑定客户端时,将事件处理程序的所有权转移给客户端作为 std::unique_ptr<fidl::AsyncEventHandler<Protocol>> 的实现。拆解完成后,事件处理脚本将被销毁。从事件处理脚本析构函数中销毁任何客户端回调所引用的用户对象是安全的。
以下示例展示了此格式:
void OwnedEventHandler(async_dispatcher_t* dispatcher, fidl::ClientEnd<Echo> client_end) {
// Define some blocking futures to maintain a consistent sequence of events
// for the purpose of this example. Production code usually won't need these.
std::promise<void> teardown;
std::future<void> teardown_complete = teardown.get_future();
std::promise<void> reply;
std::future<void> got_reply = reply.get_future();
// Define the event handler for the client. The event handler is always
// placed in a |std::unique_ptr| in the owned event handler pattern.
// When the |EventHandler| is destroyed, we know that binding teardown
// has completed.
class EventHandler : public fidl::AsyncEventHandler<Echo> {
public:
explicit EventHandler(std::promise<void>& teardown, std::promise<void>& reply)
: teardown_(teardown), reply_(reply) {}
void on_fidl_error(fidl::UnbindInfo error) override {
// This handler is invoked by the bindings when an error causes it to
// teardown prematurely. Note that additionally cleanup is typically
// performed in the destructor of the event handler, since both manually
// initiated teardown and error teardown will destroy the event handler.
std::cerr << "Error in Echo client: " << error;
// In this example, we abort the process when an error happens. Production
// code should handle the error gracefully (by cleanly exiting or attempt
// to recover).
abort();
}
~EventHandler() override {
// Additional cleanup may be performed here.
// Notify the outer function.
teardown_.set_value();
}
// Regular event handling code is also supported.
void OnString(fidl::Event<Echo::OnString>& event) override {
std::string response(event.response().data(), event.response().size());
std::cout << "Got event: " << response << std::endl;
}
void OnEchoStringResponse(fuchsia_examples::EchoEchoStringResponse& response) {
std::string reply(response.response().data(), response.response().size());
std::cout << "Got response: " << reply << std::endl;
if (!notified_reply_) {
reply_.set_value();
notified_reply_ = true;
}
}
private:
std::promise<void>& teardown_;
std::promise<void>& reply_;
bool notified_reply_ = false;
};
std::unique_ptr handler = std::make_unique<EventHandler>(teardown, reply);
EventHandler* handler_ptr = handler.get();
// Create a client that owns the event handler.
fidl::SharedClient client(std::move(client_end), dispatcher, std::move(handler));
// Make an EchoString call, passing it a callback that captures the event
// handler.
client->EchoString({"hello"}).ThenExactlyOnce(
[handler_ptr](fidl::Result<Echo::EchoString>& result) {
ZX_ASSERT(result.is_ok());
auto& response = result.value();
handler_ptr->OnEchoStringResponse(response);
});
got_reply.wait();
// Make another call but immediately start binding teardown afterwards.
// The reply may race with teardown; the callback is always canceled if
// teardown finishes before a response is received.
client->EchoString({"hello"}).ThenExactlyOnce(
[handler_ptr](fidl::Result<Echo::EchoString>& result) {
if (result.is_ok()) {
auto& response = result.value();
handler_ptr->OnEchoStringResponse(response);
} else {
// Teardown finished first.
ZX_ASSERT(result.error_value().is_canceled());
}
});
// Begin tearing down the client.
// This does not have to happen on the dispatcher thread.
client.AsyncTeardown();
teardown_complete.wait();
}
自定义拆解观察器
向绑定提供 fidl::AnyTeardownObserver 的实例。
拆解完成后,观察者会收到通知。您可以通过多种方式创建拆解观测器:
fidl::ObserveTeardown接受任意可调用对象,并将其封装在拆解观察器中:
fidl::SharedClient<Echo> client;
// Let's say |my_object| is constructed on the heap;
MyObject* my_object = new MyObject;
// ... and needs to be freed via `delete`.
auto observer = fidl::ObserveTeardown([my_object] {
std::cout << "client is tearing down" << std::endl;
delete my_object;
});
// |my_object| may implement |fidl::AsyncEventHandler<Echo>|.
// |observer| will be notified and destroy |my_object| after teardown.
client.Bind(std::move(client_end), dispatcher, my_object, std::move(observer));
fidl::ShareUntilTeardown接受std::shared_ptr<T>,并安排绑定在拆解后销毁其共享引用:
fidl::SharedClient<Echo> client;
// Let's say |my_object| is always managed by a shared pointer.
std::shared_ptr<MyObject> my_object = std::make_shared<MyObject>();
// |my_object| will be kept alive as long as the binding continues
// to exist. When teardown completes, |my_object| will be destroyed
// only if there are no other shared references (such as from other
// related user objects).
auto observer = fidl::ShareUntilTeardown(my_object);
client.Bind(std::move(client_end), dispatcher, my_object.get(), std::move(observer));
用户可以创建与其他指针类型(例如 fbl::RefPtr<T>)搭配使用的自定义拆解观察器。
SharedClient 适用于业务逻辑状态由框架管理的系统(驱动程序就是一个示例,其中驱动程序运行时是管理框架)。在这种情况下,绑定运行时和框架将共同拥有用户对象:绑定运行时将通知框架它已放弃所有用户对象引用,此时框架可以安排销毁用户对象,但前提是同一组对象没有其他正在进行的异步拆解进程。异步拆解不需要在任意面向用户的调用之间进行同步,有助于防止出现死锁。
先启动拆解,然后在拆解完成后销毁用户对象的模式有时称为两阶段关闭。
简单决策树
如果您不确定要使用哪种客户端类型,请遵循以下经验法则:
如果您的应用是单线程的,请使用
Client。如果您的应用是多线程的,但由多个
Client如果您的应用是多线程的,并且无法保证 FIDL 客户端从各自的调度程序中使用:请使用
SharedClient并承担两阶段关闭的复杂性。
服务器端线程
fidl::Client 和 fidl::SharedClient 都会在销毁时拆除绑定。与客户端不同,服务器端没有用于拆除绑定的 RAII 类型。原因是,简单应用中的服务器是响应客户端发起的连接尝试而创建的,并且通常会一直存在,继续处理客户端请求,直到客户端关闭其端点。当应用关闭时,用户可能会关闭异步调度程序,然后该调度程序会同步拆除与其关联的所有服务器绑定。
不过,随着应用变得越来越复杂,在某些情况下,需要主动关闭服务器实现对象,这涉及拆除服务器绑定。例如,驱动程序需要在设备移除时停止相关服务器。
服务器可以通过以下两种方式主动在其端拆除绑定:
fidl::ServerBindingRef::Close或fidl::ServerBindingRef::Unbind。SomeCompleter::Close,其中SomeCompleter是提供给服务器方法处理程序的 method completer。
如需了解其语义的精确参考信息,请参阅服务器标头中的相关文档。
上述所有方法都只会启动拆解,因此可以安全地与正在进行的操作或并行 to-user 调用(例如方法处理程序)竞争。因此,我们需要谨慎维护服务器实现对象的生命周期。有两种情况:
从同步的调度程序启动拆解
如果传递给 fidl::BindServer 的异步调度程序 (async_dispatcher_t*) 是 同步调度程序,并且清理是从在该调度程序上运行的任务(例如从服务器方法处理程序内)启动的,则在 Unbind/Close 返回后,绑定不会对服务器对象进行任何调用。此时可以安全地销毁服务器对象。
如果指定了未绑定处理程序,则绑定将在不久后(通常在事件循环的下一次迭代中)进行一次最终的面向用户调用,即未绑定处理程序。未绑定处理程序具有以下签名:
// |impl| is the pointer to the server implementation.
// |info| contains the reason for binding teardown.
// |server_end| is the server channel endpoint.
// |Protocol| is the type of the FIDL protocol.
void OnUnbound(ServerImpl* impl, fidl::UnbindInfo info,
fidl::ServerEnd<Protocol> server_end) {
// If teardown is manually initiated and not due to an error, |info.ok()| will be true.
if (info.ok())
return;
// Handle errors...
}
如果服务器对象之前已被销毁,则回调不得访问 impl 变量,因为该变量现在指向无效内存。
从任意线程启动拆解
如果应用无法保证始终从同步调度程序启动拆解,那么在拆解期间可能会有正在进行的 to-user 调用。为防止释放后使用,我们可能会实现与客户端类似的双阶段关闭模式。
假设为每个传入的连接请求在堆上分配一个服务器对象:
// Create an instance of our EchoImpl that destroys itself when the connection closes.
new EchoImpl(dispatcher, std::move(server_end));
我们可以在 unbound_handler 回调结束时销毁服务器对象。在此示例中,代码通过在回调结束时删除堆分配的服务器来实现此目的。
class EchoImpl {
public:
// Bind this implementation to a channel.
EchoImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<fuchsia_examples::Echo> server_end)
: binding_(fidl::BindServer(dispatcher, std::move(server_end), this,
// This is a fidl::OnUnboundFn<EchoImpl>.
[this](EchoImpl* impl, fidl::UnbindInfo info,
fidl::ServerEnd<fuchsia_examples::Echo> server_end) {
if (info.is_peer_closed()) {
FX_LOGS(INFO) << "Client disconnected";
} else if (!info.is_user_initiated()) {
FX_LOGS(ERROR) << "Server error: " << info;
}
delete this;
})) {}
// Later, when the server is shutting down...
void Shutdown() {
binding_->Unbind(); // This stops accepting new requests.
// The server is destroyed asynchronously in the unbound handler.
}
};
两阶段关闭模式是必要的,因为在启动拆解时,可能会有并行的服务器方法处理程序调用。绑定运行时会在这些从用户到内核的调用返回后调用未绑定的处理程序。特别是,如果服务器方法处理程序需要很长时间才能返回,则解除绑定过程可能会延迟相同的时间。建议将长时间运行的处理程序工作分流到线程池,并通过 completer.ToAsync() 异步回复,从而确保方法处理程序及时返回并及时解除绑定。如果服务器绑定在此期间被拆除,则会舍弃相应回复。
与异步调度程序交互
所有异步请求/响应处理、事件处理和错误处理都是通过绑定客户端或服务器时提供的 async_dispatcher_t* 完成的。除了关闭调度程序之外,您可以预期 to-user 调用将在调度程序线程上执行,并且不会嵌套在其他用户代码中(没有重入问题)。
如果在有任何有效绑定时关闭调度程序,则拆解可能会在执行关闭的线程上完成。因此,在执行 async::Loop::Shutdown/async_loop_shutdown 时,您不得获取提供给 fidl::SharedClient 的拆解观察器或提供给 fidl::BindServer 的未绑定处理程序可能获取的任何锁。(您可能应该确保在关闭时不会持有任何锁,因为该方法会加入所有调度程序线程,而这些线程可能会在用户代码中获取锁)。