新的 C++ 繫結可容納多種執行緒模型。視乎 應用程式的架構,則有不同的類別和用途 可供選擇本文件將說明 FIDL 的工具和技術 非主流執行緒環境
背景:FIDL 連線的生命週期
在 FIDL 連線的生命週期內,發生這類事件。 也就是如何確保執行緒安全,以及避免使用釋放後記憶體:
繫結呼叫:這些是使用者程式碼在 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 呼叫:
- 排程:確保刪除相關使用者物件 一律不會與任何使用者呼叫同時排程
- 參考計數:參照計算使用者物件, 會在繫結拆除完成後刪除。
- 兩階段關機:如果在繫結拆解時發出通知 以便使用者將業務物件 之後就會刪除
C++ 繫結原生支援上述所有方法,參考資料為 是一種選擇功能 繫結。
用戶端執行緒
支援非同步作業的用戶端類型有兩種:fidl::Client
和 fidl::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::Client
和 fidl::SharedClient
都會拆解繫結
破壞。與用戶端不同,伺服器端沒有 RAII 類型
拆解繫結原因在於較簡單的應用程式
為回應用戶端嘗試連線而建立,且
持續處理用戶端要求,直到用戶端關閉
端點應用程式關閉時,使用者可能會關閉非同步作業
然後以同步方式拆解所有相關聯的伺服器繫結
。
然而,隨著應用程式日趨複雜,也會有主動提供的情境 關閉伺服器實作物件,包括移除 伺服器繫結例如,在發生問題時 裝置已移除。
伺服器可透過以下兩種方式自行終止繫結:
fidl::ServerBindingRef::Close
或fidl::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
。(您可能已經
確保關閉時沒有任何鎖定,因為一切加入所有
調度器執行緒可能會遭到使用者程式碼鎖定)。