歡迎!您可能不喜歡以 C++ 編寫程式碼,描述多步驟非同步作業。
fpromise::promise<>
[1]
makes this a bit easier. 本指南涵蓋非同步控制流程程式設計的常見問題,並提供 fpromise::promise<> 程式庫中解決這些問題的常見使用模式。
非同步程式碼的難處為何?
在 fpromise::promise<> 程式庫中,非同步工作定義為由多個具有明確暫停點的同步程式碼區塊組成。
定義非同步工作時,必須解決下列問題:
表達控制流程:如何表達同步區塊的序列,以及資料在這些區塊之間的流動方式?如何以淺顯易懂的方式說明?
狀態和資源管理:支援工作執行需要哪些中繼狀態,以及必須擷取哪些外部資源?如何表達這類感受?如何安全地表達?
術語
fpromise::promise<>是僅供移動的物件,由一組 Lambda 或回呼組成,用於描述非同步工作,最終會產生值或錯誤。- 處理常式函式是在建立 Promise 時提供的回呼。
- 「續傳函式」是提供給現有 Promise 上各種「續傳方法」的回呼。
fpromise::executor負責排定及執行 Promise。承諾必須先轉移至fpromise::executor,才會執行。此時,執行程式會負責排程和執行作業。fpromise::context可視需要傳遞至處理常式和續傳函式,以存取fpromise::executor,以及低階暫停和繼續控制項。
建立及執行第一個 fpromise::promise<>
我們來編寫簡單的 Promise。
#include <lib/fpromise/promise.h>
...
fpromise::promise<> p = fpromise::make_promise([] {
// This is a handler function.
auto world_is_flat = AssessIfWorldIsFlat();
if (world_is_flat) {
return fpromise::error();
}
return fpromise::ok();
});
p 現在包含描述簡單工作的 Promise。
如要執行 Promise,必須在 fpromise::executor 的實作項目中排定 Promise。最常用的執行器是 async::Executor
[2]
,會在 async_dispatcher_t 上排定回呼。為進行測試和探索,這裡也使用 fpromise::single_threaded_executor 和相關聯的方法 fpromise::run_single_threaded()[3]。
// When a promise is scheduled, the `fpromise::executor` takes ownership of it.
fpromise::result<> result = fpromise::run_single_threaded(std::move(p));
assert(result.is_ok());
建立更複雜的 fpromise::promise<>
傳回、錯誤類型和解決狀態
如上所述,fpromise::promise<> 的範本引數代表回傳和錯誤型別:
fpromise::promise<ValueType, ErrorType>
您可以省略錯誤類型,系統會採用 void 的預設錯誤類型 (例如 fpromise::promise<MyValueType> 等同於 fpromise::promise<MyValueType,
void>)。
執行期間,Promise 最終必須達到下列其中一種狀態:
- 成功:處理常式函式或最後一個接續函式 (請參閱下文) 已傳回
fpromise::ok()。 - 錯誤:處理常式函式或部分續傳函式已傳回
fpromise::error(),且沒有後續續傳函式攔截該函式。 - 已捨棄:承諾在解析為「成功」或「錯誤」之前遭到破壞。
.then()、.and_then()、.or_else():串連非同步區塊
通常複雜的任務可以分解成更細微的任務。這些工作都必須以非同步方式執行,但如果工作之間存在某些依附元件,則必須保留這些依附元件。這可透過不同的組合器達成,例如:
fpromise::promise::then()可用於定義工作依附元件,因為無論工作 1 的狀態為何,都會先執行工作 1,再執行工作 2。先前工作的結果會透過fpromise::result<ValueType, ErrorType>&或const fpromise::result<ValueType, ErrorType>&類型的引數接收。
auto execute_task_1_then_task_2 =
fpromise::make_promise([]() -> fpromise::result<ValueType, ErrorType> {
...
}).then([](fpromise::result<ValueType, ErrorType>& result) {
if (result.is_ok()) {
...
} else { // result.is_error()
...
}
});
- 只有在工作 1 成功時,
fpromise::promise::and_then()才可用於定義工作依附元件。先前工作的結果會透過ValueType&或ValueType&型別的引數接收。
auto execute_task_1_then_task_2 =
fpromise::make_promise([]() { ... }).and_then([](ValueType& success_value) {
...
});
- 只有在工作 1 失敗時,
fpromise::promise::or_else()才可用於定義工作依附元件。先前工作的結果會透過ErrorType&或const ErrorType&類型的引數接收。
auto execute_task_1_then_task_2 =
fpromise::make_promise([]() { ... }).or_else([](ErrorType& failure_value) {
...
});
fpromise::join_promises()和fpromise::join_promise_vector():並行執行
有時,多個 Promise 可以執行,彼此之間沒有依附元件,但匯總結果是下一個非同步步驟的依附元件。在本例中,fpromise::join_promises() 和 fpromise::join_promise_vector() 用於加入多個 Promise 的結果。
如果每個 Promise 都由變數參照,則會使用 fpromise::join_promises()。fpromise::join_promises() 支援異質 Promise 型別。先前工作的結果會透過 std::tuple<...>& 或 const
std::tuple<...>& 型別的引數接收。
auto DoImportantThingsInParallel() {
auto promise1 = FetchStringFromDbAsync("foo");
auto promise2 = InitializeFrobinatorAsync();
return fpromise::join_promises(std::move(promise1), std::move(promise2))
.and_then([](std::tuple<fpromise::result<std::string>,
fpromise::result<Frobinator>>& results) {
return fpromise::ok(std::get<0>(results).value() +
std::get<1>(results).value().GetFrobinatorSummary());
});
}
如果承諾儲存在 std::vector<> 中,就會使用 fpromise::join_promise_vector()。這項作業還有一項額外限制,就是所有 Promise 都必須是同質 (屬於相同類型)。先前工作的結果會透過 std::vector<fpromise::result<ValueType, ErrorType>>& 或 const std::vector<fpromise::result<ValueType, ErrorType>>& 型別的引數接收。
auto ConcatenateImportantThingsDoneInParallel() {
std::vector<fpromise::promise<std::string>> promises;
promises.push_back(FetchStringFromDbAsync("foo"));
promises.push_back(FetchStringFromDbAsync("bar"));
return fpromise::join_promise_vector(std::move(promises))
.and_then([](std::vector<fpromise::result<std::string>>& results) {
return fpromise::ok(results[0].value() + "," + results[1].value());
});
}
return fpromise::make_promise():透過傳回新的 Promise 進行鏈結或分支
在執行階段之前,延後決定要鏈結哪些 Promise 可能會很有用。這個方法與語法上執行的鏈結 (透過連續使用 .then()、.and_then() 和 .or_else() 呼叫) 不同。
處理常式函式可以傳回新的 Promise,而非 fpromise::result<...> (使用 fpromise::ok 或 fpromise::error),系統會在處理常式函式傳回後評估該 Promise。
fpromise::make_promise(...)
.then([] (fpromise::result<>& result) {
if (result.is_ok()) {
return fpromise::make_promise(...); // Do work in success case.
} else {
return fpromise::make_promise(...); // Error case.
}
});
這個模式也有助於將可能很長的 Promise 分解為較小的可讀取區塊,例如讓延續函式從上述範例傳回 DoImportantThingsInParallel() 的結果。
宣告並保持中間狀態
有些工作只需要在 Promise 處於待處理或執行狀態時保持運作。這個狀態需要共用,因此不適合移至任何指定的 Lambda,而且由於生命週期與 Promise 相關聯,因此也不適合將擁有權轉移至存留時間較長的容器。
雖然不是唯一解決方案,但同時使用 std::unique_ptr<> 和 std::shared_ptr<> 是常見模式:
std::unique_ptr<>
fpromise::promise<> MakePromise() {
struct State {
int i;
};
// Create a single std::unique_ptr<> container for an instance of State and
// capture raw pointers to the state in the handler and continuations.
//
// Ownership of the underlying memory is transferred to a lambda passed to
// `.inspect()`. |state| will die when the returned promise is resolved or is
// abandoned.
auto state = std::make_unique<State>();
state->i = 0;
return fpromise::make_promise([state = state.get()] { state->i++; })
.and_then([state = state.get()] { state->i--; })
.inspect([state = std::move(state)](const fpromise::result<>&) {});
}
std::shared_ptr<>
fpromise::promise<> MakePromise() {
struct State {
int i;
};
// Rely on shared_ptr's reference counting to destroy |state| when it is safe
// to do so.
auto state = std::make_shared<State>();
state->i = 0;
return fpromise::make_promise([state] { state->i++; }).and_then([state] {
state->i--;
});
}
fpromise::scope:放棄承諾,避免違反記憶體安全規定
fpromise::scope 可將 fpromise::promise<> 的生命週期繫結至記憶體中的資源。例如:
#include <lib/fpromise/scope.h>
class A {
public:
fpromise::promise<> MakePromise() {
// Capturing |this| is dangerous: the returned promise will be scheduled
// and executed in an unknown context. Use |scope_| to protect against
// possible memory safety violations.
//
// The call to `.wrap_with(scope_)` abandons the promise if |scope_| is
// destroyed. Since |scope_| and |this| share the same lifecycle, it is safe
// to capture |this|.
return fpromise::make_promise([this] {
// |foo_| is critical to the operation!
return fpromise::ok(foo_.Frobinate());
})
.wrap_with(scope_);
}
private:
Frobinator foo_;
fpromise::scope scope_;
};
void main() {
auto a = std::make_unique<A>();
auto promise = a->MakePromise();
a.reset();
// |promise| will not run any more, even if scheduled, protected access to the
// out-of-scope resources.
}
fpromise::sequencer:在個別 Promise 完成時封鎖 Promise
TODO: you can .wrap_with(sequencer) to block this promise on the completion of the last promise wrapped with the same sequencer object
#include <lib/fpromise/sequencer.h>
// TODO
fpromise::bridge:與以回呼為基礎的非同步函式整合
TODO: fpromise::bridge is useful to chain continuation off a callback-based async function
#include <lib/fpromise/bridge.h>
// TODO
fpromise::bridge:將單一延續鏈的執行作業解除耦合
TODO: fpromise::bridge is also useful to decouple one chain of continuation into two
promises that can be executed on different fpromise::executor instances
常見問題
and_then 或 or_else 序列必須具有相容的型別
使用 and_then 建構 Promise 時,每個後續的續集可能會有不同的 ValueType,但必須具有相同的 ErrorType,因為 and_then 會轉送先前的錯誤,但不會消耗這些錯誤。
使用 or_else 建構 Promise 時,每個後續延續項目可能會有不同的 ErrorType,但必須具有相同的 ValueType,因為 or_else 會轉送先前的值,但不會耗用這些值。
如要在序列中途變更型別,請使用 then 消耗先前的結果,並產生所需型別的新結果。
以下範例不會編譯,因為最後一個 and_then 處理常式傳回的錯誤類型與先前處理常式的結果不相容。
auto a = fpromise::make_promise([] {
// returns fpromise::result<int, void>
return fpromise::ok(4);
}).and_then([] (const int& value) {
// returns fpromise::result<float, void>
return fpromise::ok(value * 2.2f);
}).and_then([] (const float& value) {
// ERROR! Prior result had "void" error type but this handler returns const
// char*.
if (value >= 0)
return fpromise::ok(value);
return fpromise::error("bad value");
}
使用 then 消耗結果並變更其類型:
auto a = fpromise::make_promise([] {
// returns fpromise::result<int, void>
return fpromise::ok(4);
}).and_then([] (const int& value) {
// returns fpromise::result<float, void>
return fpromise::ok(value * 2.2f);
}).then([] (const fpromise::result<float>& result) -> fpromise::result<float, const char*> {
if (result.is_ok() && result.value() >= 0)
return fpromise::ok(value);
return fpromise::error("bad value");
}
處理常式 / 接續函式可以傳回 fpromise::result<> 或新的 fpromise::promise<>,但不能同時傳回兩者
您可能會想編寫處理常式,在一個條件分支中傳回 fpromise::promise<>,在另一個條件分支中傳回 fpromise::ok() 或 fpromise::error()。這是違法的,因為編譯器無法將 fpromise::result<> 轉換為 fpromise::promise<>。
解決方法是傳回會解析為所需結果的 fpromise::promise<>:
auto a = fpromise::make_promise([] {
if (condition) {
return MakeComplexPromise();
}
return fpromise::make_ok_promise(42);
});
續約簽名
你是否看過類似這樣的錯誤訊息?
../../sdk/lib/fit-promise/include/lib/fpromise/promise_internal.h:342:5: error: static_assert failed "The provided handler's last argument was expected to be of type V& or const V& where V is the prior result's value type and E is the prior result's error type. Please refer to the combinator's documentation for
a list of supported handler function signatures."
或是:
../../sdk/lib/fit-promise/include/lib/fpromise/promise.h:288:5: error: static_assert failed due to requirement '::fpromise::internal::is_continuation<fpromise::internal::and_then_continuation<fpromise::promise_impl<fit::function_impl<16, false, fpromise::result<fuchsia::modular::storymodel::StoryModel, void> (fpromise::context &)> >, (lambda at ../../src/modular/bin/sessionmgr/story/model/ledger_story_model_storage.cc:222:17)>, void>::value' "Continuation type is invalid. A continuation is a callable object with this signature: fpromise::result<V, E>(fpromise::context&)."
這很可能表示其中一個續集函式的簽章無效。不同接續函式的有效簽章如下所示:
針對 .then():
.then([] (fpromise::result<V, E>& result) {});
.then([] (const fpromise::result<V, E>& result) {});
.then([] (fpromise::context& c, fpromise::result<V, E>& result) {});
.then([] (fpromise::context& c, const fpromise::result<V, E>& result) {});
針對 .and_then():
.and_then([] (V& success_value) {});
.and_then([] (const V& success_value) {});
.and_then([] (fpromise::context& c, V& success_value) {});
.and_then([] (fpromise::context& c, const V& success_value) {});
針對 .or_else():
.or_else([] (E& error_value) {});
.or_else([] (const E& error_value) {});
.or_else([] (fpromise::context& c, E& error_value) {});
.or_else([] (fpromise::context& c, const E& error_value) {});
針對 .inspect():
.inspect([] (fpromise::result<V, E>& result) {});
.inspect([] (const fpromise::result<V, E>& result) {});
擷取和引數生命週期
Promise 由處理常式和續傳函式組成,通常是 Lambda。建構 lambda 擷取清單時,請務必謹慎,以免擷取在相關處理常式或續傳作業執行時無效的記憶體。
舉例來說,這個 Promise 會擷取保證在 Foo() 傳回時失效的記憶體 (因此,在排定及執行傳回的 Promise 時)。
fpromise::promise<> Foo() {
int i;
return fpromise::make_promise([&i] {
i++; // |i| is only valid within the scope of Foo().
});
}
實際程式碼中的例項會更細緻。稍微不那麼明顯的例子:
fpromise::promise<> Foo() {
return fpromise::make_promise(
[i = 0] { return fpromise::make_promise([&i] { i++; }); });
}
fpromise::promise 會急切地刪除處理常式和續傳函式:最外層的處理常式傳回最內層的處理常式後,就會遭到刪除。如要瞭解此情況的正確模式,請參閱上文的「宣告並保持中繼狀態有效」。
>>> 區段撰寫
- 從一種錯誤類型轉換為另一種
- fpromise::bridge
- 常見問題: 擷取的狀態生命週期