web-dev-qa-db-ja.com

スタックレスコルーチンは、スタックフルコルーチンとどのように異なりますか?

背景:

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

提案された解決策:

これを実現するには、各ワーカースレッドをコルーチンに置き換えるコルーチンベースのアプローチのように思えます。その後、実際の(カーネル)ワーカースレッドのプールによってバックアップされた作業キューを持つことができます。アイテムが処理のために特定のコルーチンのキューに配置されると、エントリがスレッドプールのキューに配置されます。その後、対応するコルーチンを再開し、キューに入れられたデータを処理してから、再び一時停止して、ワーカースレッドを解放して他の作業を行います。

実装の詳細:

これをどのように行うかを考えると、スタックレスコルーチンとスタックフルコルーチンの機能の違いを理解するのが困難です。 Boost.Coroutine ライブラリを使用してスタックフルコルーチンを使用した経験があります。概念レベルから理解するのは比較的簡単です:各コルーチンについて、CPUコンテキストとスタックのコピーを維持し、コルーチンに切り替えると、保存されたコンテキストに切り替わります(カーネルモードスケジューラのように) )。

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

したがって、スタックレスコルーチンとスタックフルコルーチンの機能の違いをより明確に理解したいと思います。具体的には、これらの質問について考えます。

  • このような参照 は、スタックフルコスタックとスタックレスコルーチンのどちらで降伏/再開できるかで区別されることを示唆しています。これは事実ですか?スタックレスコルーチンでできるが、スタックレスコルーチンではできないことの簡単な例はありますか?

  • 自動ストレージ変数(つまり、 "スタック上の"変数)の使用に制限はありますか?

  • スタックレスコルーチンから呼び出すことができる機能に制限はありますか?

  • スタックレスコルーチンのスタックコンテキストの保存がない場合、コルーチンの実行中に自動ストレージ変数はどこに移動しますか?

52
Jason R

まず、 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(???);
}

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

スタックフルコルーチンは通常、ランタイムスタックに対応するために一定量のメモリを割り当てる必要があり(十分に大きくする必要があります)、コンテキストスイッチはスタックレスに比べて高価です。 Boost.Coroutineは40サイクルかかりますが、スタックレスコルーチンが復元する必要があるのはプログラムカウンターだけなので、CO2は平均してわずか7サイクルです。

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

スタックレスコルーチンについて言えば、それはランタイムスタックがないことを意味するものではなく、ホスト側と同じランタイムスタックを使用することを意味するだけなので、再帰関数も呼び出すことができます。すべての再帰はホストのランタイムスタックで発生します。対照的に、スタックフルコルーチンでは、再帰関数を呼び出すと、コルーチンのスタックで再帰が発生します。

質問に答えるには:

  • 自動ストレージ変数(つまり、 "スタック上の"変数)の使用に制限はありますか?

いいえ。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のソースコードを確認するだけで、内部の仕組みを理解できる場合があります;)

47
Jamboree

必要なのはユーザーランドのスレッド/ファイバーです。通常、ネストされた深いコールスタック(たとえば、TCP接続からのメッセージの解析)で(ファイバーで実行されている)コードを中断します。この場合、スタックレスコンテキストスイッチングは使用できません(アプリケーションスタックはスタックレスコルーチン間で共有されます->呼び出されたサブルーチンのスタックフレームは上書きされます)。

Boost.contextに基づいてユーザーランドのスレッド/ファイバーを実装するboost.fiberのようなものを使用できます。

2
olk