コルーチン関数が呼び出され、中断され、再開され、終了したときに呼び出される一連の操作に関するドキュメント(cppreferenceと機能自体の標準ドキュメント)を読み込もうとしていました。ドキュメントは、ライブラリ開発者がライブラリコンポーネントを使用してコルーチンの動作をカスタマイズできるようにするさまざまな拡張ポイントの概要を詳しく説明しています。高レベルでは、この言語機能は非常によく考えられているようです。
残念ながら、コルーチン実行のメカニズムを理解するのに本当に苦労しています。ライブラリ開発者として、私はさまざまな拡張ポイントを使用してコルーチンの実行をカスタマイズする方法を理解できません。またはどこから始めればよいか。
以下の関数は、私が完全には理解していない新しいカスタマイズポイントのセットに含まれています。
initial_suspend()
return_void()
return_value()
await_ready()
await_suspend()
await_resume()
final_suspend()
unhandled_exception()
誰かが高レベルの疑似コード、つまりユーザーコルーチンを実行したときにコンパイラーが生成するコードを記述できますか?抽象レベルでは、await_suspend
、await_resume
、await_ready
、await_transform
、return_value
などの関数がいつ呼び出されたかを把握しようとしています。それらが役立つ目的とコルーチンライブラリを書くためにそれらをどのように使用できるか。
これが主題外であるかどうかはわかりませんが、ここで紹介するリソースは、コミュニティ全体にとって非常に役立ちます。 cppcoroのようなライブラリ実装にグーグルして飛び込んでも、この最初の障壁を乗り越えるのに役立ちません:(
N4775 は、C++ 20のコルーチンの提案の概要を示しています。さまざまなアイデアを紹介しています。以下は https://dwcomputersolutions.net にある私のブログからです。詳細については、他の投稿を参照してください。
Hello Worldコルーチンプログラム全体を調べる前に、さまざまな部分を段階的に説明します。これらには以下が含まれます:
ファイル全体がこの投稿の最後に含まれています。
_Future f()
{
co_return 42;
}
_
コルーチンをインスタンス化します
_ Future myFuture = f();
_
これは、値_42
_を返すだけの単純なコルーチンです。キーワード_co_return
_が含まれているため、コルーチンです。キーワード_co_await
_、_co_return
_、または_co_yield
_を持つ関数はコルーチンです。
最初に気づくのは、整数を返すものの、コルーチンの戻り型が(ユーザー定義の)型Futureであることです。その理由は、コルーチンを呼び出すときに、関数を現在実行するのではなく、オブジェクトを初期化して、最終的には、私たちが探しているAKAの値を取得するためです。
コルーチンをインスタンス化するとき、コンパイラーが最初に行うことは、この特定のタイプのコルーチンを表すpromiseタイプを見つけることです。
テンプレートの部分的な特殊化を作成することにより、どのプロミスタイプがどのコルーチン関数シグネチャに属しているかをコンパイラに伝えます
_template <typename R, typename P...>
struct coroutine_trait
{};
with a member called `promise_type` that defines our Promise Type
_
この例では、次のようなものを使用できます。
_template<>
struct std::experimental::coroutines_v1::coroutine_traits<Future> {
using promise_type = Promise;
};
_
ここでは、_coroutine_trait
_の特殊化を作成し、パラメーターを指定せず、戻りタイプFuture
を指定します。これは、Future f(void)
のコルーチン関数シグネチャと完全に一致します。この場合、_promise_type
_はプロミスタイプで、この場合は_struct Promise
_です。
これでユーザーになりました。コルーチンライブラリは、Futureクラス自体で_coroutine_trait
_を指定する簡単な方法を提供するため、通常は独自の_promise_type
_特殊化を作成しません。詳細は後ほど。
以前の投稿で述べたように、コルーチンは一時停止と再開が可能なため、ローカル変数を常にスタックに格納できるとは限りません。スタックセーフではないローカル変数を格納するために、コンパイラはヒープにContextオブジェクトを割り当てます。 Promiseのインスタンスも保存されます。
コルーチンは、外の世界と通信できない限り、ほとんど役に立ちません。私たちの約束は、将来のオブジェクトが他のコードがコルーチンと対話することを可能にする一方で、コルーチンがどのように振る舞うかを教えてくれます。その後、PromiseとFutureは、コルーチンハンドルを介して互いに通信します。
単純なコルーチンの約束は次のようになります。
_struct Promise
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {};
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}
int val;
bool done;
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}
_
述べたように、コルーチンがインスタンス化され、コルーチンの全寿命の間終了するときに、約束が割り当てられます。
完了すると、コンパイラは_get_return_object
_を呼び出します。このユーザー定義関数は、Futureオブジェクトを作成し、それをコルーチンインスタティエーターに返します。
この例では、Futureがコルーチンと通信できるようにしたいので、コルーチンのハンドルを使用してFutureを作成します。これにより、私たちの未来は私たちの約束にアクセスできるようになります。
コルーチンを作成したら、すぐに実行を開始するか、すぐに中断したままにするかを知る必要があります。これは、Promise::initial_suspend()
関数を呼び出すことによって行われます。この関数は、別の投稿で調べるAwaiterを返します。
この場合、関数をすぐに開始したいので、_suspend_never
_を呼び出します。関数を中断した場合は、ハンドルのresumeメソッドを呼び出してコルーチンを開始する必要があります。
コルーチンで_co_return
_演算子が呼び出されたときに何をすべきかを知る必要があります。これは_return_value
_関数を介して行われます。この場合、後でFutureを介して取得できるように、Promiseに値を保存します。
例外が発生した場合、何をすべきかを知る必要があります。これは_unhandled_exception
_関数によって行われます。この例では、例外は発生しないはずなので、単に中止します。
最後に、コルーチンを破棄する前に何をすべきかを知る必要があります。これは_final_suspend function
_を介して行われます。この場合、結果を取得したいので_suspend_always
_を返します。次に、コルーチンはコルーチンハンドルdestroy
メソッドを介して破棄する必要があります。そうでない場合、_suspend_never
_を返すと、コルーチンは実行が終了するとすぐにそれ自体を破棄します。
ハンドルは、コルーチンとその約束へのアクセスを提供します。 2つのフレーバーがあります。promiseにアクセスする必要がないときのvoidハンドルと、promiseにアクセスする必要があるときのためのpromiseタイプを持つコルーチンハンドルです。
_template <typename _Promise = void>
class coroutine_handle;
template <>
class coroutine_handle<void> {
public:
void operator()() { resume(); }
//resumes a suspended coroutine
void resume();
//destroys a suspended coroutine
void destroy();
//determines whether the coroutine is finished
bool done() const;
};
template <Promise>
class coroutine_handle : public coroutine_handle<void>
{
//gets the promise from the handle
Promise& promise() const;
//gets the handle from the promise
static coroutine_handle from_promise(Promise& promise) no_except;
};
_
将来はこのようになります:
_class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle)
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;
};
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
_
Futureオブジェクトは、コルーチンを外部の世界に抽象化する責任があります。 promiseの_get_return_object
_実装に従って、promiseからハンドルを取得するコンストラクターがあります。
デストラクタはコルーチンを破棄します。なぜなら、私たちのケースでは、コントロールがpromiseの寿命であるからです。
最後に次の行があります:
_using promise_type = Promise;
_
コルーチンの戻りクラスで_coroutine_trait
_を定義した場合、C++ライブラリを使用すると、上記のように独自の_promise_type
_を実装する必要がなくなります。
そして、それがあります。私たちの最初のシンプルなコルーチン。
_
#include <experimental/coroutine>
#include <iostream>
struct Promise;
class Future;
using Handle = std::experimental::coroutines_v1::coroutine_handle<Promise>;
struct Promise
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {};
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}
int val;
bool done;
};
class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle)
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
//The Co-routine
Future f()
{
co_return 42;
}
int main()
{
Future myFuture = f();
std::cout << "The value of myFuture is " << myFuture() << std::endl;
return 0;
}
_
_co_await
_演算子を使用すると、コルーチンを一時停止して、コルーチンの呼び出し元に制御を戻すことができます。これにより、操作が完了するのを待つ間、他の作業を行うことができます。完了したら、中断したところから再開できます。
_co_await
_演算子が右側の式を処理する方法はいくつかあります。ここでは、最も単純なケースを検討します。ここで、_co_await
_式がAwaiterを返します。
Awaiterは単純なstruct
またはclass
であり、次のメソッドを実装します:_await_ready
_、_await_suspend
_および_await_resume
_。
bool await_ready() const {...}
は、コルーチンを再開する準備ができているかどうか、またはコルーチンの一時停止を確認する必要があるかどうかを単に返します。 _await_ready
_がfalseを返すと仮定します。 _await_suspend
_の実行に進みます
_await_suspend
_メソッドでは、いくつかのシグネチャを使用できます。最も単純なのはvoid await_suspend(coroutine_handle<> handle) {...}
です。これは、_co_await
_が中断するコルーチンオブジェクトのハンドルです。この関数が完了すると、コルーチンオブジェクトの呼び出し元に制御が戻ります。コルーチンが永遠に中断されたままにならないように、後でコルーチンハンドルを保存するのはこの関数です。
handle.resume()
が呼び出されると、 _await_ready
_はfalseを返します。または他のメカニズムがコルーチンを再開し、メソッドauto await_resume()
が呼び出されます。 _await_resume
_からの戻り値は、_co_await
_演算子が返す値です。上記のように、_co_await expr
_のexprがawaiterを返すのが現実的でない場合があります。 expr
がクラスを返す場合、クラスはAwaiter operator co_await (...) which will return the Awaiter. Alternatively one can implement an
await_transform _method in our
_ promise_type`の独自のインスタンスを提供し、変換するexprアウェイターに。
Awaiterについて説明したので、_initial_suspend
_の_final_suspend
_メソッドと_promise_type
_メソッドの両方がAwaiterを返すことを指摘しておきます。オブジェクト_suspend_always
_および_suspend_never
_は簡単な待機者です。 _suspend_always
_は_await_ready
_にtrueを返し、_suspend_never
_はfalseを返します。ただし、自分のロールアウトを妨げるものは何もありません。
Awaiterの実際の様子に興味がある場合は、 私の将来のオブジェクト を見てください。コルーチンハンドルを後の処理のためにラムダに格納します。