新增了 C++ 绑定线程指南

新的 C++ 绑定支持各种线程模型。取决于 架构,则有不同的类和用法 可供选择的样式本文档介绍了使用 FIDL 的工具和技术 非常重要的线程处理中。

背景:FIDL 连接的生命周期

在 FIDL 连接的生命周期内, 从线程安全以及防止释放后使用的角度出发:

图:用户代码对 FIDL 绑定对象 ( binding) 调用 to-binding 调用
在用户代码上调用用户,拆解操作会取消所有
这些

  • 绑定调用:这些是用户代码在 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 调用之间起到排除作用:

  1. 时间安排:确保销毁相关用户对象 绝不会与任何用户调用并行调度。
  2. 引用计数:用户对象的引用计数,以便 它们在绑定拆解完成之前不会被销毁。
  3. 两阶段关停:在绑定拆解时提供通知 完成之后,用户可以将业务对象排列为 销毁。

C++ 绑定本身支持上述所有方法。引用计数是 在某些情况下不合适,因此在使用 以及绑定

客户端线程处理

有两种客户端类型支持异步操作:fidl::Clientfidl::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::Clientfidl::SharedClient 都会拆解绑定 破坏。与客户端不同,服务器端没有 来拆解绑定。原因在于更简单的应用中的服务器 为响应客户端的连接尝试而创建,并且通常会保持 继续处理客户端请求,直到客户端关闭其 端点。应用关闭时,用户可能会关闭异步 然后,调度程序会同步删除 。

不过,随着应用变得越来越复杂,一些情况下可以主动 关闭服务器实现对象,其中包括删除 服务器绑定例如,驱动程序需要在出现以下情况时停止相关服务器: 设备已移除。

服务器可以通过两种方式自愿拆解他们那端的绑定:

  • fidl::ServerBindingRef::Closefidl::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。(您应该 这样可确保任何情况下都不会在关停时持有锁 调度程序线程,这些线程可能会锁定用户代码)。