全新 C++ 繫結執行緒指南

新的 C++ 繫結可容納各種不同的執行緒模型。視應用程式的架構而定,您可以選擇不同的類別和使用樣式。本文件說明在非基本執行緒環境中使用 FIDL 的工具和技術。

背景:FIDL 連線的壽命

在 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 是不安全的做法,而用戶端仍可同時分派訊息。這通常是使用者從非調度程式執行緒刪除 MyDevice (或其他業務物件) 的情況,也就是並非針對目前的 FIDL 繫結監控及分派訊息的執行緒。

在處理事件和處理來自伺服器的方法呼叫時,同樣的使用「使用後」的風險也類似。

解決方法有幾個,就是在刪除使用者物件與對使用者呼叫之間加入雙向排除:

  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_;
};

在上述範例中,如果伺服器想要重新初始化 bar_,同時保持 FooProtocol 連線運作,則可能會在處理 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 是提供給伺服器方法處理常式的方法完成工具。

如需精確的語意參考資料,請參閱伺服器標頭中的相關說明文件。

上述所有方法都只會啟動拆除作業,因此可能會透過進行中的作業或平行「對使用者」呼叫 (例如方法處理常式) 安全地競爭。因此需要謹慎處理,維護伺服器實作物件的生命週期。有兩種情況:

正在從同步的調度工具開始拆除

使用者的「 進 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* 完成。除了關閉調度工具外,您可以預期對使用者的呼叫將在調度工具執行緒上執行,而非以巢狀結構嵌入其他使用者程式碼 (沒有重複問題)。

如果在有任何使用中的繫結時關閉調度工具,系統可能會在執行關閉的執行緒上完成拆除作業。因此,您不得採用任何可能會被提供給 fidl::SharedClient 的拆解觀察器 (或提供給 fidl::BindServer 的未繫結處理常式) 被取用,或在執行 async::Loop::Shutdown/async_loop_shutdown 時避免在關閉周圍保留任何鎖定 (因為可能會彙整所有調派程式執行緒,而這可能會鎖定使用者程式碼中,因此)。