web-dev-qa-db-ja.com

C ++ 20のコルーチンの仕組みは何ですか?

コルーチン関数が呼び出され、中断され、再開され、終了したときに呼び出される一連の操作に関するドキュメント(cppreferenceと機能自体の標準ドキュメント)を読み込もうとしていました。ドキュメントは、ライブラリ開発者がライブラリコンポーネントを使用してコルーチンの動作をカスタマイズできるようにするさまざまな拡張ポイントの概要を詳しく説明しています。高レベルでは、この言語機能は非常によく考えられているようです。

残念ながら、コルーチン実行のメカニズムを理解するのに本当に苦労しています。ライブラリ開発者として、私はさまざまな拡張ポイントを使用してコルーチンの実行をカスタマイズする方法を理解できません。またはどこから始めればよいか。

以下の関数は、私が完全には理解していない新しいカスタマイズポイントのセットに含まれています。

  • initial_suspend()
  • return_void()
  • return_value()
  • await_ready()
  • await_suspend()
  • await_resume()
  • final_suspend()
  • unhandled_exception()

誰かが高レベルの疑似コード、つまりユーザーコルーチンを実行したときにコンパイラーが生成するコードを記述できますか?抽象レベルでは、await_suspendawait_resumeawait_readyawait_transformreturn_valueなどの関数がいつ呼び出されたかを把握しようとしています。それらが役立つ目的とコルーチンライブラリを書くためにそれらをどのように使用できるか。


これが主題外であるかどうかはわかりませんが、ここで紹介するリソースは、コミュニティ全体にとって非常に役立ちます。 cppcoroのようなライブラリ実装にグーグルして飛び込んでも、この最初の障壁を乗り越えるのに役立ちません:(

23
Curious

N4775 は、C++ 20のコルーチンの提案の概要を示しています。さまざまなアイデアを紹介しています。以下は https://dwcomputersolutions.net にある私のブログからです。詳細については、他の投稿を参照してください。

Hello Worldコルーチンプログラム全体を調べる前に、さまざまな部分を段階的に説明します。これらには以下が含まれます:

  1. コルーチンの約束
  2. コルーチンのコンテキスト
  3. コルーチンの未来
  4. コルーチンのハンドル
  5. コルーチン自体
  6. 実際にコルーチンを使用するサブルーチン

ファイル全体がこの投稿の最後に含まれています。

コルーチン

_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の実際の様子に興味がある場合は、 私の将来のオブジェクト を見てください。コルーチンハンドルを後の処理のためにラムダに格納します。

15
doron