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

新的 C++ 繫結可容納多種執行緒模型。視乎 應用程式的架構,則有不同的類別和用途 可供選擇本文件將說明 FIDL 的工具和技術 非主流執行緒環境

背景:FIDL 連線的生命週期

在 FIDL 連線的生命週期內,發生這類事件。 也就是如何確保執行緒安全,以及避免使用釋放後記憶體:

圖:使用者程式碼會在 FIDL 繫結物件、繫結物件上叫用繫結呼叫
叫用使用者程式碼上的使用者呼叫,Teardown 可全部取消
那些

  • 繫結呼叫:這些是使用者程式碼在 FIDL 訊息上發出的呼叫 物件,例如從 FIDL 執行階段的角度傳入。例如:

    • 在用戶端發出 FIDL 方法呼叫屬於繫結呼叫。
    • 透過完整工具從伺服器實作發出回覆, 繫結呼叫。
  • 使用者呼叫:FIDL 執行階段對使用者物件發出的呼叫 (包括使用者提供的回呼),例如從觀點傳出 FIDL 執行階段的初始狀態例如:

    • 伺服器訊息調度器在伺服器上叫用 FIDL 方法處理常式 是向使用者呼叫的
    • 將雙向 FIDL 方法的回應傳送給 FIDL 用戶端 也稱之為使用者呼叫
    • 錯誤處理常式也是使用者呼叫的。

    使用者通話有時也稱為「電話接通」。由於 User 物件 位於繫結的 FIDL 繫結之上一層以便做好準備 從自己的專業觀點進行討論

  • 拆解:會停止分派郵件的動作。尤其是如果 拆解完成,繫結不會再發出使用者呼叫; 繫結呼叫將失敗或產生無效效果。例如:

    • 分派期間發生錯誤。
    • 刪除 {fidl,fdf}::[Wire]Client
    • 正在撥打 {fidl,fdf}::[Wire]SharedClient::AsyncTeardown()

    Teardown 通常會導致用戶端/伺服器端點關閉。

  • 解除繫結:停止郵件分派並額外復原的動作 收發訊息時使用的用戶端/伺服器端點。執行 所以務必要拆解例如:

    • 正在撥打 fidl::ServerBindingRef::Unbind()

拆卸期間使用釋放後風險

刪除一組相關物件 (包括 FIDL 用戶端或伺服器) 時, 必須負責排序刪除程序,以便與使用者發出 FIDL 繫結執行階段最終不會呼叫已刪除的物件。

具體範例如下:假設 MyDevice 物件擁有 FIDL 用戶端, 發出多次雙向 FIDL 呼叫,並傳遞擷取 this 做為 每次都會傳回結果回呼刪除 MyDevice 並不安全, 用戶端仍在傳送郵件給用戶端。這通常是 在使用者從伺服器刪除MyDevice或其他業務物件時 非調度器執行緒,亦即非調度工具監控和調度 訊息。

在處理事件與處理事件時,存在類似的釋放風險 處理來自伺服器的方法呼叫

有一些解決方案以達成這個問題,也就是 排除刪除使用者物件與 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 不是事件處理常式。相反地,User 物件 用戶端擁有事件處理介面,並有可能實作事件處理介面,並傳遞 借用指標指向用戶端物件。

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。 刪除 Pod 後,請在回呼中操控擷取的 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();
}

自訂拆卸觀察器

為繫結提供 fidl::AnyTeardownObserver 的例項。 摧毀完成時,觀察器會收到通知。以下提供 建立卸除觀察器的方法:

  • fidl::ObserveTeardown 接受任意呼叫,並包裝在 Teardown 觀察器:
  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 是提供給 伺服器方法處理常式

如需精確的語義參考資料,請參閱 伺服器標頭

上述所有方法都只會啟動拆解作業,因此進行中的競賽可能會 作業或平行「對使用者」呼叫 (例如方法處理常式)。因此 權衡利弊得失為 為了維持生命週期 伺服器實作物件的定義。會發生以下兩種情況:

啟動同步調度工具拆除作業

當傳遞至 fidl::BindServer 的非同步調度工具 (async_dispatcher_t*) 時 是 同步調度工具 ,然後拆除 由該調度器上執行的工作啟動 (例如,從伺服器內部 方法處理常式),則繫結不會對伺服器物件發出任何呼叫 Unbind/Close 傳回後。此時您可以放心刪除伺服器物件 點。

如果指定了未繫結的處理常式,繫結「會」成為最終版本 to-user 呼叫,這個方法不久後就會是未繫結處理常式,通常會在下一個 事件迴圈疊代。未繫結的處理常式具有下列簽章:

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

必須要有兩階段關閉模式, 要求啟動中斷時平行伺服器方法處理常式呼叫。 繫結執行階段會在這些 to-user 呼叫之後呼叫未繫結的處理常式 傳回。特別是,如果伺服器方法處理常式的傳回時間過長, 取消繫結程序也可能會延遲相同的時間。是 將長時間執行的處理常式工作卸載至執行緒集區,並 請透過 completer.ToAsync() 以非同步方式回覆,以確保傳回 方法處理常式並且及時取消繫結。如果伺服器連線,回覆將遭到捨棄 。

與非同步調度器互動

所有非同步要求/回應處理、事件處理和錯誤處理 是透過提供的 async_dispatcher_t* 來完成繫結用戶端作業 伺服器除了關閉調度工具之外,您可以預期 to-user 呼叫將在調度器執行緒上執行,不會在內部巢狀結構內 其他的使用者程式碼 (無重試問題)。

如果您在任何有效繫結時關閉調度工具,請卸除 但可能會在執行關閉的執行緒上完成因此,您不得利用 任何可能由提供給拆除觀察器提供的 fidl::SharedClient 或提供給 fidl::BindServer 的未繫結處理常式, 正在執行 async::Loop::Shutdown/async_loop_shutdown。(您可能已經 確保關閉時沒有任何鎖定,因為一切加入所有 調度器執行緒可能會遭到使用者程式碼鎖定)。