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

新的 C++ 绑定适用于各种线程模型。您可以根据应用的架构选择不同的类和使用样式。本文档介绍了在非琐碎的线程环境中使用 FIDL 的工具和技术。

背景知识:FIDL 连接的生命周期

在 FIDL 连接的生命周期内,从线程安全性和防止释放后使用方面来看,以下情况非常重要:

图:用户代码对 FIDL 绑定对象调用 to-binding 调用,绑定对用户代码调用 to-user 调用,拆解会取消所有这些调用

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

    • 对客户端进行 FIDL 方法调用是一种绑定调用。
    • 使用补全符从服务器实现发出回复也是一个 to-binding 调用。
  • 向用户的调用:这些是 FIDL 运行时对用户对象(包括用户提供的回调)进行的调用,即从 FIDL 运行时的角度来看是出站调用。例如:

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

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

  • 拆解:停止消息调度的操作。具体而言,在拆解完成后,绑定将不再进行任何对用户的调用;对绑定的调用将失败或产生无效/琐碎的效果。示例:

    • 调度期间发生错误。
    • 销毁 {fidl,fdf}::[Wire]Client
    • 正在呼叫{fidl,fdf}::[Wire]SharedClient::AsyncTeardown()

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

  • 解绑:用于停止消息调度,以及恢复用于发送和接收消息的客户端/服务器端点的操作。这样做必然会涉及到拆解。示例:

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

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

销毁一组相关对象(包括 FIDL 客户端或服务器)时,必须小心地按顺序销毁它们,以便 FIDL 绑定运行时发出的向用户调用不会最终调用已销毁的对象。

举个具体示例,假设 MyDevice 对象拥有一个 FIDL 客户端,并进行多次双向 FIDL 调用,每次都传递一个捕获 this 的 lambda 作为结果回调。在客户端可能仍在调度消息期间销毁 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() 加入该线程并关闭循环。从技术层面来看,这里涉及两个线程,但从互斥访问的角度来看,这是安全的,并且 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 会接受任意可调用对象,并将其封装在拆解观察器中:
  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 是提供给服务器方法处理程序的方法补全器。

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

上述所有方法仅会发起拆解,因此可以安全地与正在进行的操作或并行向用户调用(例如方法处理脚本)竞争。因此,我们需要进行权衡,在维护服务器实现对象的生命周期方面需要格外小心。有两种情况:

从同步调度程序启动拆解

同步调度程序,并且通过该调度程序运行的任务(例如从服务器方法处理程序内)发起了拆解,那么在 Unbind/Close 返回后,绑定将不会对服务器对象执行任何调用。async_dispatcher_t*fidl::BindServer此时,您可以安全地销毁该服务器对象。

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

// |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 的未绑定处理程序获取的任何锁。无论如何,您应该确保在关闭期间不持有任何锁,因为关闭会加入所有调度程序线程,而这些线程可能会在用户代码中获取锁。