fef::promise<> 使用手冊

歡迎使用!您可能不喜歡以 C++ 編寫程式碼來說明多步驟非同步作業。

fpromise::promise<> [1] 可以簡化這項作業。本指南說明非同步控制流程程式設計的常見問題,並提供常見的使用模式,以便解決 fpromise::promise<> 程式庫中的這些問題。

非同步程式碼的難度為何?

fpromise::promise<> 程式庫中,非同步工作的定義為由多個包含明確暫停點的「同步」區塊所組成。

定義非同步工作時,必須有解決以下問題的解決方案:

  1. 執行控制流程:同步區塊的「順序」如何?兩者間的資料流動方式為何?如何以淺顯易懂的方式完成?

  2. 管理狀態和資源:執行工作所需的中繼狀態,以及必須擷取哪些外部資源?這種形式的表達方式以及如何安全地完成?

術語

  • fpromise::promise<> 是移動式物件,由一組 lambda 或回呼組成,用於描述最終會產生值或錯誤的非同步工作。
  • 處理常式函式是在建立承諾時提供的回呼。
  • 接續函式是一種回呼,能用於現有承諾的各種接續方法
  • fpromise::executor 負責排程和執行承諾。Promise 擁有權轉移給 fpromise::executor 後才會執行。此時,執行程式會負責其排程和執行作業。
  • 您可以選擇將 fpromise::context 傳遞至處理常式和接續函式,以便取得 fpromise::executor 的存取權,以及降低層級暫停和繼續控制項的存取權。

正在建構及執行第一個 fpromise::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 實作上排程。最常用的執行程式是 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():鏈結非同步區塊

複雜的工作通常可以分解為較精細的較小工作。這些工作都必須以非同步方式執行,但如果工作之間有某些相依性,就一定要保留這些工作。這可以透過不同的組合器達成,例如:

  • 無論工作 1 的狀態為何,fpromise::promise::then() 都會有助於定義工作依附元件,例如依序執行工作 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 時,系統會使用 fpromise::join_promises()fpromise::join_promises() 支援異質保證類型。先前工作的結果會透過 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()。這新增了限制,規定所有承諾都必須是同質性 (相同類型)。先前工作的結果會透過 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():傳回新的承諾來鏈結或分支

將承諾鏈結至執行階段之前的決策延遲可能很有用。這個方法與語法執行 (透過使用連續的 .then().and_then().or_else() 呼叫) 而執行的鏈結相反。

處理常式函式可能會傳回新的 promise,再執行處理常式函式傳回後評估,而不會傳回 fpromise::result<...> (使用 fpromise::okfpromise::error)。

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

這個模式也有助於將可能的長期承諾內容分解為較小可讀取的區塊,例如接續函式傳回上述範例中 DoImportantThingsInParallel() 的結果。

宣告並保持中間狀態有效

某些工作要求狀態只會保持運作,直到其本身為待處理或執行中為止。這個狀態因需要共用而不適合移至任何指定的 lambda,也不適合將擁有權移轉給使用時間較久的容器,原因是想要將其生命週期與承諾的容器搭配使用。

雖然這不是唯一的解決方案,但同時使用 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:在個別承諾中封鎖承諾

待辦事項:您可以 .wrap_with(sequencer) 封鎖這個承諾,直到最後一個包含相同排序器物件的 promise 完成為止

#include <lib/fpromise/sequencer.h>
// TODO

fpromise::bridge:整合以回呼為基礎的非同步函式

任務:fpromise::bridge 與回呼型非同步函式的鏈結非常相似,可用於鏈結接續函式

#include <lib/fpromise/bridge.h>
// TODO

fpromise::bridge:分離單一接續鏈的執行作業

待辦事項:fpromise::bridge 也可用來將一個接續鏈分離成兩個可在不同 fpromise::executor 執行個體上執行的承諾。

常見問題

and_thenor_else 的順序必須具有相容的類型

使用 and_then 建構承諾時,每個連續接續的 ValueType 可能會有不同的 ValueType,但必須具有相同的 ErrorType,因為 and_then 會在不使用的情況下轉送先前的錯誤。

使用 or_else 建構承諾時,每個連續的連續性可能都有不同的 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 擷取清單時必須謹慎,避免在執行問題時擷取處理常式或接續記憶體無效的記憶體。

例如,此承諾會擷取 Foo() 傳回時間保證為無效的記憶體,也就是排定及執行傳回的承諾。

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
  • 常見錯誤做法 也就是擷取的狀態生命週期