3

メッセージ ポンプ スレッド プール アーキテクチャに基づくアプリケーションがあります。ブロックする可能性のあるアクションがある場合は常に、「完了/トリガー evnet でのコールバック」アクションとして実装されるため、実行中のスレッドがストールすることはありません。

この手法はほとんどの場合に適していますが、コードが非常に複雑になり、非常に不便になる場合があります。

私ができるようにしたいのは、関数を待機中の前後の部分に分割することなく、透過的な方法で待機中にイベントを処理し続けることです。

どうすればいいですか?

私は2つの選択肢を念頭に置いていました:

  1. 待機中に実行中の関数内からメッセージ ループを実行します。
  2. 待機中に新しい作業スレッドを作成し、再開時に (適切な方法で) 終了します。

いくつか例を挙げると、どちらのオプションにも欠点があります。

1 の場合:

  • スタック オーバーフローが発生する可能性があります。
  • デッドロック状態になる可能性があります。
  • 内側のメッセージが 2 番目のイベントの完了を待機する結果となり、その間に外側のイベントが完了すると、外側の関数は 2 番目のイベントが完了するまで続行できず、この状況が拡大する可能性があります。

オプション 2 は、単純にますます多くのスレッドを作成することになります。

もちろん、私が思いもよらなかった他のオプションがあるかもしれません。

編集:言語は C++ であるため、簡単な (移植可能な?) 方法で関数をステップアウトおよびステップインすることはできません。プラットフォームは Windows (API) ですが、関係ないと思います。

4

5 に答える 5

2

ポータブル C++ の場合、これは機能しませんが、プラットフォームが Windows であると述べたので、MsgWaitForMultipleObjectsを使用しないのはなぜですか? その目的は、質問の内容を正確に実行できるようにすることです。待機中にメッセージを送り続けます。

于 2009-06-22T11:09:29.840 に答える
0

あなたの問題は根本的なものであり、C++とは関係がないようです。スタックの使用を隠すには他の言語の方がおそらく優れていますが、Foo()から戻っていない限り、Foo()の呼び出しスタックが必要です。また、Bar()も実行している場合は、それにもコールスタックが必要です。

各スレッドには独自のコールスタックが付属しているため、スレッドはこれに対する優れたアプローチです。継続は、コールスタックを保存するための賢いが複雑な方法であるため、利用可能な場合はそれらもオプションです。ただし、それらが必要ない場合は、1つのコールスタックで処理する必要があります。

1つのコールスタックでDalingするには、再入可能性に対処する必要があります。ここでは、何が可能かについての一般的な答えはありません。一般に、関数F1 ... Fyによって処理される一連のメッセージM1..Mxがあり、アプリケーション固有の、場合によっては状態依存のマッピングがあります。再入可能なメッセージループでは、Mjを受信したときにFiを実行している可能性があります。今問題は何をすべきかです。すべての関数F1...Fnが呼び出せるわけではありません。特に、Fi自体は呼び出せない場合があります。ただし、リソースを共有しているなどの理由で、他の一部の機能も使用できない場合があります。これはアプリケーションに依存します。

Mjの処理でこれらの使用できない関数のいずれかが必要な場合は、延期する必要があります。キュー内の次のメッセージを受け入れることができますか?繰り返しになりますが、これは実装に依存し、メッセージの種類や内容に関連する場合もあります。メッセージが十分に独立している場合、それらを順不同で実行する可能性があります。これはすぐにかなり複雑になります。キュー内のN番目のメッセージを受け入れることができるかどうかを判断するには、先行するN-1メッセージに対して順不同で実行できるかどうかを確認する必要があります。

言語は依存関係を隠さないことであなたを助けることができますが、最終的には明示的な決定をしなければなりません。特効薬はありません。

于 2009-06-22T10:51:32.077 に答える
0

特定のアプリケーション (つまり、メッセージの処理にかかる時間など) について詳しく知らないと、多くのハンドウェーブが発生します。

  • これはマネージまたはアンマネージ C++ ですか?

  • どの ThreadPool を使用していますか?

    • QueueUserWorkItem?
    • CreateIoCompletionPort 経由の独自のプールですか?
    • それとも Vista の SubmitThreadpoolWork ですか?

スレッドプールの性質が重要であるため、プラットフォームはある程度関連していると思います。

例えば:

スレッド プール (つまり、CreateIoCompletionPort) に( Completion Ports )を使用する場合。同時に実行されるスレッドの数 (したがって、最終的に作成される合計スレッド数) をある程度制御できます。同時スレッドの最大数を 4 に設定すると、Windows は 4 つのスレッドの同時実行のみを許可しようとします。4 つのスレッドすべてが処理中でビジーで、5 番目の項目をキューに入れると、Windows は 4 つのうちの 1 つが終了するまで (スレッドを再利用して) その項目の実行を許可しません。この規則が破られるのは、スレッドがブロックされている (つまり、I/O を待機している) ときだけであり、その後、より多くのスレッドの実行が許可されます。

これは、Completion Ports について理解しておくべき重要なことであり、プラットフォームが関連する理由です。カーネルを使用せずにこのようなものを実装することは非常に困難です。使用中のスレッドとブロックされたスレッドの違いを知るには、スレッドの状態にアクセスする必要があります。完了ポートは、カーネルへのコンテキスト スイッチの数に関しても非常に効率的です。

質問に戻る:

メッセージを処理/ディスパッチするには1つのスレッドが必要であり、メッセージ処理はすべてワーカーをスレッドプールにプッシュすることによって処理されるようです。Completion ポートに負荷分散と同時実行性を処理させます。メッセージ処理ループがブロックされることはなく、メッセージの処理を続行できます。

着信メッセージのレートが処理能力をはるかに超えている場合は、おそらくキューのサイズに注意を払い、キューが大きくなりすぎたときにブロックする必要があります。

于 2009-06-21T15:16:09.563 に答える
0

編集:「関数を事前/事後待機部分に分割」したくないと述べています。

どの言語で開発していますか? 継続がある場合 ( yield returnC# の場合)、手続き型のように見えるが、ブロック操作が完了コールバックを行うまで簡単に一時停止できるコードを記述する方法が提供されます。

このアイデアに関する記事は次のとおりです: http://msdn.microsoft.com/en-us/magazine/cc546608.aspx

アップデート:

残念ながら、言語は C++ です。

それは素晴らしいTシャツのスローガンになるでしょう.

シーケンシャル コードをステート マシンとして構造化すると、割り込み/再開が可能になります。

たとえば、あなたの痛みは、開始する関数と完了イベントのハンドラーとして機能する関数の 2 つの関数を記述する必要があります。

void send_greeting(const std::string &msg)
{
    std::cout << "Sending the greeting" << std::endl;
    begin_sending_string_somehow(msg, greeting_sent_okay);
}

void greeting_sent_okay()
{
    std::cout << "Greeting has been sent successfully." << std::endl;
}

あなたのアイデアは待つことでした:

void send_greeting(const std::string &msg)
{
    std::cout << "Sending the greeting" << std::endl;

    waiter w;
    begin_sending_string_somehow(msg, w);
    w.wait_for_completion();

    std::cout << "Greeting has been sent successfully." << std::endl;
}

その例では、waiteroperator() をオーバーロードして、コールバックとして機能できるようにwait_for_completionし、operator() が呼び出されたことを確認するまで何らかの方法でハングアップします。

begin_sending_string_somehowの2番目のパラメーターは、パラメーターを受け入れない呼び出し可能な型にすることができるテンプレートパラメーターであると想定しています。

しかし、おっしゃる通り、これには欠点があります。スレッドがそのように待機しているときはいつでも、別の潜在的なデッドロックを追加し、スレッド全体とそのスタックの「リソース」も消費しています。つまり、作業を実行できるようにするために、別の場所でより多くのスレッドを作成する必要があります。 、これはスレッドプールの要点と矛盾しています。

代わりに、クラスを作成します。

class send_greeting
{
    int state_;
    std::string msg_;

public:
    send_greeting(const std::string &msg)
        : state_(0), msg_(msg) {}

    void operator()
    {
        switch (state_++)
        {
            case 0:
                std::cout << "Sending the greeting" << std::endl;
                begin_sending_string_somehow(msg, *this);
                break;

            case 1:
                std::cout << "Greeting has been sent successfully." 
                          << std::endl;
                break;
        }
    }
};

このクラスは、関数呼び出し operator を実装します()。呼び出されるたびに、ロジックの次のステップが実行されます。(もちろん、些細な例であるため、これはほとんどが状態管理のノイズですが、4 つまたは 5 つの状態を持つより複雑な例では、コードのシーケンシャルな性質を明確にするのに役立つ場合があります)。

問題:

  • イベント コールバック関数のシグネチャに特別なパラメーターがある場合はoperator()、追加のフィールドにパラメーターを格納し、パラメーターなしのオーバーロードを呼び出す の別のオーバーロードを追加する必要があります。これらのフィールドは、初期状態での実行時には意味がなくても、初期状態ではコンパイル時にアクセスできるため、その後、乱雑になり始めます。

  • クラスのオブジェクトはどのように構築および削除されますか? オブジェクトは、操作が完了するか放棄されるまで存続する必要があります... C++ の中心的な落とし穴です。それを管理するための一般的なスキームを実装することをお勧めします。「削除する必要があるもの」のリストを作成し、これが特定の安全なポイントで自動的に行われるようにします。つまり、できる限り GC に近づけるようにします。そこから離れれば離れるほど、より多くのメモリがリークします。

于 2009-06-20T23:47:48.033 に答える
0

あなたの問題は、スレッドの同期ですよね?それがあなたの問題なら、なぜミューテックスを使わないのですか? インターフェースでまとめることができます。実際、PIMPL イディオムを使用してミューテックスを移植可能にすることができます。

http://msdn.microsoft.com/en-us/library/system.threading.mutex(VS.71).aspx

于 2009-06-23T17:01:21.690 に答える