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

新的 C++ 绑定包含丰富多样的线程模型。根据应用的架构,有不同的类和使用样式可供选择。本文档将介绍在重要线程环境中使用 FIDL 的工具和技术。

背景:FIDL 连接的生命周期

在 FIDL 连接的生命周期内,从线程安全性的角度来看,这些情况非常重要,并会阻止释放后使用:

图:用户代码对 FIDL 绑定对象调用绑定调用,绑定调用用户代码与用户代码的绑定调用,拆解会取消所有这些调用

  • 绑定调用:这些是用户代码对 FIDL 消息传递对象进行的调用,即从 FIDL 运行时的角度来看的入站调用。例如:

    • 在客户端上调用 FIDL 方法属于绑定调用。
    • 使用完整器从服务器实现进行回复也属于绑定调用。
  • 向用户调用:这些是 FIDL 运行时对用户对象(包括用户提供的回调)进行的调用,即从 FIDL 运行时的角度发出的调用。例如:

    • 在服务器实现上调用 FIDL 方法处理程序的服务器消息调度程序属于对用户调用。
    • FIDL 客户端通过回调将响应传递给双向 FIDL 方法,也属于面向用户的调用。
    • 错误处理程序也是对用户的调用。

    面向用户的调用有时也称为“向上调用”,因为从绑定的角度来看,用户对象位于 FIDL 绑定之上的一层。

  • 收尾清理:停止邮件发送的操作。特别是,拆解完成后,绑定将不再发出对用户的调用;绑定调用将失败或产生无效/细微的效果。示例:

    • 调度时出错。
    • 销毁 {fidl,fdf}::[Wire]Client
    • 正在呼叫{fidl,fdf}::[Wire]SharedClient::AsyncTeardown()

    拆解通常会导致客户端/服务器端点关闭。

  • 取消绑定:停止发送消息,并恢复用于收发消息的客户端/服务器端点的操作。这样做必然涉及拆解。示例:

    • 正在呼叫fidl::ServerBindingRef::Unbind()

拆解期间释放后使用的风险

销毁一组相关对象(包括 FIDL 客户端或服务器)时,必须谨慎排序,确保 FIDL 绑定运行时进行的对用户进行的调用不会最终调用已销毁的对象。

举一个具体的例子:假设一个 MyDevice 对象拥有 FIDL 客户端并进行了多次双向 FIDL 调用,同时传递一个 lambda 来捕获 this 作为结果回调。在此期间,客户端可能仍在发送消息时销毁 MyDevice 是不安全的。当用户从非调度程序线程(即,不是针对当前 FIDL 绑定监控和调度消息的线程)销毁 MyDevice(或其他业务对象)时,通常就会发生这种情况。

处理事件和处理来自服务器的方法调用时,在销毁时也存在类似的“释放后使用”风险。

此问题有几种解决方案,都是在销毁用户对象和向用户调用之间增加相互排斥的理念:

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

C++ 绑定本身支持上述所有方法。在某些情况下,引用计数是不恰当的,因此它是使用绑定时的选择启用功能。

客户端线程处理

支持异步操作的客户端类型有两种:fidl::Clientfidl::SharedClient。如需精确参考其语义,请参阅客户端标头中的文档。

fidl::Client 的线程行为也适用于 fidl::WireClient。同样,fidl::SharedClient 的线程行为扩展到 fidl::WireSharedClient

客户端

- fidl::Client

  • 您只能从在该调度程序上运行的任务中调用 FIDL 方法。
  • 客户端对象本身无法移至其他对象,而该对象随后会从在其他调度程序上运行的任务中销毁。

这样可确保在调度 FIDL 消息或错误事件时,包含的用户对象不会被销毁。它适用于单线程和面向对象的使用方式。

fidl::Client 只能与同步异步调度程序搭配使用。async::Loop 的一种特定用法是通过 loop.StartThread() 创建单个工作器线程,然后通过 loop.Shutdown() 从另一个线程联接该工作器线程并通过 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_;
};

在上述示例中,如果服务器想要在使 FooProtocol 连接保持活跃状态的同时重新初始化 bar_,可以在处理 FooMethod 时使用 ThenExactlyOnce 回复取消错误,或引入重试逻辑。

SharedClient

fidl::SharedClient 支持解决方案 2(引用计数)解决方案 3(两阶段关闭)。您可以从任意线程对 SharedClient 进行 FIDL 调用,并使用任何类型的异步调度程序中的共享客户端。与销毁客户端会立即保证不再向用户进行的调用的 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();
}

自定义拆解观察器

向绑定提供 fidl::AnyTeardownObserver 实例。拆解完成后,观察器会收到通知。您可以通过多种方式创建拆解观察器:

  • fidl::ObserveTeardown 接受任意 Callable 函数,并将其封装在拆解观察器中:
  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 都会在销毁时拆解绑定。与客户端不同,服务器端没有可拆解绑定的 RAII 类型。原因在于较简单的应用中的服务器是为响应客户端发起的连接尝试而创建的,并且通常会绕开继续处理客户端请求的情况,直到客户端关闭其端点。当应用关闭时,用户可以关闭异步调度程序,然后同步调度程序与之关联的所有服务器绑定。

但是,随着应用变得越来越复杂,有一些场景需要主动关闭服务器实现对象,这涉及拆解服务器绑定。例如,移除设备时,驱动程序需要停止相关服务器。

服务器可以通过以下两种方法主动解除绑定:

  • fidl::ServerBindingRef::Closefidl::ServerBindingRef::Unbind
  • SomeCompleter::Close,其中 SomeCompleter 是向服务器方法处理程序提供的方法完成器。

如需精确参考其语义,请参阅服务器标头中的文档。

上述所有方法都只会启动拆解,因此可能会安全地与正在进行的操作或并行的用户调用(例如方法处理程序)竞争。因此,需要权衡的是,在维持服务器实现对象的生命周期时需要谨慎。有两种情况:

从已同步的调度程序启动拆解

async_dispatcher_t*fidl::BindServerUnbindClose此时销毁服务器对象是安全的。

如果指定了未绑定的处理程序,则绑定很快(通常是在事件循环的下一次迭代)进行一次最终的向用户调用,即未绑定的处理程序。未绑定处理程序具有以下签名:

// |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.
  }
};

为了适应在发起拆解时并行服务器方法处理程序调用的可能性,必须使用两阶段关停模式。这些向用户的调用返回后,绑定运行时将调用未绑定的处理程序。特别是,如果服务器方法处理程序需要很长时间才能返回,则解除绑定过程可能会延迟相同的时间。建议将长时间运行的处理程序工作分流到线程池,并通过 completer.ToAsync() 进行异步回复,从而确保及时返回方法处理程序并及时取消绑定。如果服务器绑定在此期间已被关闭,则回复将被舍弃。

与异步调度程序交互

所有异步请求/响应的处理、事件处理和错误处理都是通过绑定客户端或服务器时提供的 async_dispatcher_t* 完成的。除关闭调度程序外,面向用户的调用将在调度程序线程上执行,而不会嵌套在其他用户代码中(不存在重入问题)。

如果您在存在任何活跃绑定时关闭调度程序,则可能会在执行关闭的线程上完成拆解。因此,在执行 async::Loop::Shutdown/async_loop_shutdown 时,您不得获取提供给 fidl::SharedClient 的拆解观察器或提供给 fidl::BindServer 的未绑定处理程序所能获取的任何锁(您应该确保在关闭之前不会持有任何锁,因为它会加入所有调度程序线程,而这可能会占用用户代码中的锁)。