1. ホーム
  2. c++

スタックレスコルーチンとスタックフルコルーチンはどう違うのですか?

2023-08-21 17:45:57

質問

背景は?

私は現在、多数 (数百から数千) のスレッドを持つアプリケーションを持っているので、これを尋ねています。これらのスレッドのほとんどは、キューに入れられるワーク アイテムを待っている間、非常に多くの時間アイドル状態です。ワークアイテムが利用可能になると、任意に複雑な既存のコードを呼び出して処理されます。オペレーティング システムの構成によっては、アプリケーションはユーザー プロセスの最大数を管理するカーネル パラメーターにぶつかるので、ワーカー スレッドの数を減らす手段を試してみたいと思っています。

私の提案する解決策です。

各ワーカスレッドをコルーチンに置き換える、コルーチンベースのアプローチがこれを達成するのに役立つように思われます。そして、実際の (カーネルの) ワーカースレッドのプールによってバックアップされたワークキューを持つことができます。あるアイテムが特定のコルーチンの処理キューに入れられると、スレッドプールのキューにエントリーが置かれる。そして、対応するコルーチンを再開し、そのキューに入れられたデータを処理し、それから再びそれを中断し、ワーカスレッドを他の仕事をするために解放します。

実装の詳細です。

どのようにすればいいか考えているうちに、スタックレスコルーチンとスタックフルコルーチンの機能的な違いがわからなくなってきました。スタックフルコルーチンは Boost.Coroutine ライブラリを使ってスタックフルコルーチンを使った経験があります。各コルーチンについて、CPU コンテキストとスタックのコピーを維持し、コルーチンに切り替えると、保存されたコンテキストに切り替わります (ちょうどカーネルモードのスケジューラーがそうであるように)。

私にとってあまり明確でないのは、スタックレスコルーチンがこれとどのように異なるかということです。私のアプリケーションでは、ワーク アイテムの前述のキューイングに関連するオーバーヘッドの量が非常に重要です。私が見てきたほとんどの実装、たとえば 新しいCO2ライブラリ のような、私が見たほとんどの実装は、スタックレスコルーチンがより低いオーバーヘッドのコンテキストスイッチを提供することを示唆しています。

そこで、スタックレスコルーチンとスタックフルコルーチンの機能的な差異をより明確に理解したいと思います。具体的には、以下のような質問を考えています。

  • このようなリファレンス は、スタックフルなコルーチンとスタックレスなコルーチンの違いは、どこで降伏/再開できるかにあることを示唆しています。これは事実でしょうか。スタックフルコルーチンでできて、スタックレスコルーチンでできないことの簡単な例はありますか?

  • 自動保存変数(スタック上の変数)の使用に制限はありますか?

  • スタックレスコルーチンから呼び出すことのできる関数に制限はありますか。

  • スタックレスコルーチンのスタックコンテキストの保存がない場合、コルーチンの実行時に自動保存変数はどこに行くのでしょうか。

どのように解決するのですか?

まず、このページをご覧いただきありがとうございます。 CO2 :)

Boost.Coroutineの doc には、スタックフルコルーチンの利点がよく説明されています。

スタックフル

スタックレスコルーチンとは対照的に スタックフルコルーチン はネストされたスタックフレームの中から中断することができます。 . 実行は 実行は、以前中断されたコード内のまったく同じポイントから再開されます。スタックレスコルーチンでは スタックレス・コルーチンでは、トップレベル・ルーチンのみをサスペンドすることができます。 そのトップレベルルーチンによって呼び出されたルーチンは、それ自身サスペンドすることができません。 このため、汎用ライブラリ内のルーチンでサスペンド/レジューム操作を提供することはできません。 このため、汎用ライブラリ内のルーチンにサスペンド/レジューム操作を提供することはできません。

第一種継続

ファーストクラスの継続は、引数として渡され 引数として渡され、関数から返され、データ構造に格納され データ構造に格納することができます。いくつかの実装(例えばC#のイールド)では は直接アクセスしたり、直接操作したりすることはできません。

スタックフルネスとファーストクラスのセマンティクスがなければ、いくつかの有用な実行 制御フローをサポートすることができません(例えば、協調的な マルチタスクやチェックポイントなど)。

どういうことかというと、例えば、訪問者を受け取る関数があるとします。

template<class Visitor>
void f(Visitor& v);

イテレータに変換したい場合、スタックフルコルーチンを使えば、可能です。

asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield)
{
    f(yield);
});

しかし、スタックレスコルーチンでは、そのような方法はありません。

generator<T> pull_from()
{
    // yield can only be used here, cannot pass to f
    f(???);
}

一般に、スタックフルコルーチンはスタックレスコルーチンより強力です。 では、なぜスタックレスコルーチンが必要なのでしょうか?答えは簡単、効率です。

スタックフルコルーチンは通常、ランタイムスタックを収容するために一定量のメモリを割り当てる必要があり(十分に大きくなければなりません)、コンテキストスイッチはスタックレスと比べてより高価です。例えば、私のマシンではCO2は平均で7サイクルしかかからないのに対し、Boost.Coroutineは40サイクルもかかるのですが、スタックレスコルーチンが必要とする唯一のものがプログラムカウンタであるために復元します。

とはいえ、言語サポートにより、おそらくスタックフルコルーチンは、コルーチン内に再帰がない限り、コンパイラが計算したスタックの最大サイズを利用することもできるので、メモリ使用量も改善されるでしょう。

スタックレスコルーチンと言っても、ランタイムスタックが全くないわけではなく、ホスト側と同じランタイムスタックを使うので、再帰的な関数も呼び出せるが、その再帰は全てホストのランタイムスタック上で発生することに留意してください。これに対し、スタックフルコルーチンでは、再帰関数を呼び出すと、コルーチン自身のスタック上で再帰が発生する。

質問に答えるために

  • 自動保存変数(スタック上の変数など)の使用に制限はありますか。 (例: スタック上の変数) の使用に制限はありますか?

いいえ、それは CO2 のエミュレーションの制限です。言語サポートにより、自動ストレージ変数 をコルーチンに見えるようにする は、コルーチンの内部ストレージに置かれます。コルーチンが内部で自動保存変数を使用する関数を呼び出すと、その変数はランタイムスタックに配置される。より具体的には、スタックレスコルーチンは再開後に使用できる変数/テンポラリを保持するだけでよいのです。

明確には、CO2のコルーチン本体でも自動保存変数を使用することができます。

auto f() CO2_RET(co2::task<>, ())
{
    int a = 1; // not ok
    CO2_AWAIT(co2::suspend_always{});
    {
        int b = 2; // ok
        doSomething(b);
    }
    CO2_AWAIT(co2::suspend_always{});
    int c = 3; // ok
    doSomething(c);
} CO2_END

定義がいかなる await .

  • スタックレス・コルーチンから呼び出すことのできる関数に制限はありますか? スタックレスコルーチンから呼び出せる関数に制限はありますか?

ありません。

  • スタックレスコルーチンに対してスタックコンテキストの保存がない場合。 コルーチンの実行中に自動保存変数はどこに行くのでしょうか? 実行されますか?

上記で回答したように、スタックレスコルーチンは呼び出された関数で使用される自動保存変数については気にせず、通常のランタイムスタックに置かれるだけです。

もし疑問があれば、CO2のソースコードをチェックしてみてください。)