新的 C++ 绑定支持各种线程模型。取决于 架构,则有不同的类和用法 可供选择的样式本文档介绍了使用 FIDL 的工具和技术 非常重要的线程处理中。
背景:FIDL 连接的生命周期
在 FIDL 连接的生命周期内, 从线程安全以及防止释放后使用的角度出发:
绑定调用:这些是用户代码在 FIDL 消息中发出的调用 对象,即从 FIDL 运行时的角度来看的入站。例如:
- 在客户端上调用 FIDL 方法属于绑定调用。
- 使用完成器从服务器实现进行回复也是一种 绑定调用。
用户调用:这些是 FIDL 运行时对用户对象的调用 (包括用户提供的回调),即从角度来看的出站 。例如:
- 在服务器上调用 FIDL 方法处理程序的服务器消息调度程序 是向用户致电
- 将对双向 FIDL 方法的响应传送给用户的 FIDL 客户端 也属于对用户的调用。
- 错误处理程序也是对用户的调用。
用户致电有时也称为“向上呼叫”因为用户对象是 从绑定的 FIDL 绑定您的观点。
Teardown(拆解):用于停止邮件分派的操作。特别是 拆解完成后,绑定不会再进行对用户的调用; 绑定调用将失败或产生无效/微小的影响。示例:
- 调度时出错。
- 销毁
{fidl,fdf}::[Wire]Client
。 - 正在呼叫
{fidl,fdf}::[Wire]SharedClient::AsyncTeardown()
。
拆解通常会导致客户端/服务器端点关闭。
解除绑定:停止邮件分派以及恢复消息的操作 用于发送和接收消息的客户端/服务器端点。操作 因此必然需要拆解示例:
- 正在呼叫
fidl::ServerBindingRef::Unbind()
。
- 正在呼叫
拆解期间的释放后使用风险
销毁一组相关对象(包括 FIDL 客户端或服务器)时, 对其销毁进行相应命令,使用户 FIDL 绑定运行时最终不会调用已销毁的对象。
举一个具体的示例,假设 MyDevice
对象拥有 FIDL 客户端,并且
多次进行双向 FIDL 调用,并传递 lambda 将 this
捕获为
每次调用结果回调。销毁 MyDevice
是不安全的,
在此期间客户端可能仍在调度消息。这通常是
当用户从 activity 中销毁 MyDevice
(或其他业务对象)时触发
非调度程序线程,即并非监控和调度的线程
显示当前 FIDL 绑定的消息。
在销毁时处理事件和 。
我们本着相互补充的精神,对此有几个解决办法 对用户对象销毁与 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
对象会导致释放后使用。为避免出现这种情况,请使用 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
连接保持活跃状态,它可能会使用 ThenExactlyOnce
来
在处理 FooMethod
时回复取消错误,或引入重试逻辑。
SharedClient
fidl::SharedClient
支持解决方案 2(参考
和解决方案 3(两阶段)
关停)。您可以在
从任意线程执行 SharedClient
,以及使用任何类型的共享客户端
异步调度程序的作用与立即销毁客户端的 Client
不同
可保证不再有用户调用,从而销毁 SharedClient
只是启动异步绑定拆解。用户可能会观察到
以异步方式完成拆解。进而允许移动或
将 SharedClient
克隆到与调度程序线程不同的线程,以及
销毁/调用客户端拆解,同时与用户并行执行
调用(例如响应回调)。这两项操作将竞态(响应
如果客户端提前销毁,则回调可能会被取消),但是
一旦 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();
}
自定义 teardown Observer
为绑定提供 fidl::AnyTeardownObserver
的实例。
拆解完成后,观察器会收到通知。这里有一些
创建 teardown 观察器的方法:
fidl::ObserveTeardown
接受任意 Callable 函数,并将其封装在 teardown Observer:
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
都会拆解绑定
破坏。与客户端不同,服务器端没有
来拆解绑定。原因在于更简单的应用中的服务器
为响应客户端的连接尝试而创建,并且通常会保持
继续处理客户端请求,直到客户端关闭其
端点。应用关闭时,用户可能会关闭异步
然后,调度程序会同步删除
。
不过,随着应用变得越来越复杂,一些情况下可以主动 关闭服务器实现对象,其中包括删除 服务器绑定例如,驱动程序需要在出现以下情况时停止相关服务器: 设备已移除。
服务器可以通过两种方式自愿拆解他们那端的绑定:
fidl::ServerBindingRef::Close
或fidl::ServerBindingRef::Unbind
。SomeCompleter::Close
,其中SomeCompleter
是提供给 一个服务器方法处理程序。
有关其语义的精确参考,请参阅 服务器标头。
上述所有方法仅启动拆解,因此可以安全地与进行中的比赛 操作或并行用户调用(例如方法处理程序)。因此, 需要权衡的是,在维护生命周期 服务器实现对象的组件。有两种情况:
开始从同步调度程序拆解
当异步调度程序 (async_dispatcher_t*
) 传递给 fidl::BindServer
时
是
同步调度程序
,以及拆解
由在该调度程序上运行的任务发起(例如,从服务器内部
方法处理程序),那么绑定不会对服务器对象进行任何调用
在 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
变量,因为它现在指向无效内存。
开始从任意线程拆解
如果应用无法保证始终从 则同步的调度员可能会收到正在进行的用户通话 移除过程中为防止释放后使用,我们可能会实施类似的两阶段流程, 关闭模式。
假设在堆上为每个传入连接分配了一个服务器对象 请求:
// 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.
}
};
为了保证
并行服务器方法处理程序调用。通过
绑定运行时将在向用户进行这些调用后调用未绑定的处理程序
return。具体来说,如果服务器方法处理程序需要很长时间才能返回,
解除绑定过程可能会延迟相同的时间。时间是
建议将长时间运行的处理程序工作分流到线程池,并
通过 completer.ToAsync()
异步回复,从而确保及时返回
方法处理程序并及时解除绑定。如果服务器不支持该消息,
绑定已解除。
与异步调度程序交互
所有异步请求/响应处理、事件处理和错误处理
通过在绑定客户端或客户端 ID 时提供的 async_dispatcher_t*
服务器。除了关闭调度程序外,其他情况下,
to-user 调用将在调度程序线程上执行,而不是嵌套在
其他用户代码(没有可重入问题)。
如果您在存在任何有效绑定时关闭调度程序,则拆解
可能会在执行关闭的线程上完成。因此,您不得采取
任何可能被提供给
fidl::SharedClient
,或者在发生以下情况时向 fidl::BindServer
提供的未绑定处理程序
正在执行 async::Loop::Shutdown
/async_loop_shutdown
。(您应该
这样可确保任何情况下都不会在关停时持有锁
调度程序线程,这些线程可能会锁定用户代码)。