1. ホーム
  2. c#

[解決済み] ASP.NET MVCの非同期操作では、.NET 4のThreadPoolからスレッドを使用しますか?

2022-05-05 07:49:54

質問

<ブロッククオート

この質問の後、私は非同期を使用するときに快適になります。 の操作について説明します。そこで、そのことについて2つのブログ記事を書きました。

ASP.NET MVCの非同期操作について、私の中で誤解が多すぎる。

いつもこんな文章を耳にします。 アプリケーションは、操作を非同期で実行した方がスケールが良くなります。

そして、このような文章もよく耳にしました。 1つのリクエストに対応するために2つのスレッドを余分に消費すると、他のリクエストからリソースが奪われてしまうからです。

この2つの文章は矛盾していると思うのですが。

ASP.NETでthreadpoolがどのように動作するかについてはあまり情報を持っていませんが、threadpoolにはスレッドのサイズに制限があることは知っています。だから、2番目の文章はこの問題に関連しているはずです。

そして、ASP.NET MVCの非同期操作が.NET 4のThreadPoolからスレッドを使用するかどうかを知りたいのですが?

例えば、AsyncControllerを実装した場合、アプリの構造はどうなるのでしょうか?膨大なトラフィックが発生した場合、AsyncControllerを実装するのは良いアイデアなのだろうか?

この黒い幕を取り払い、ASP.NET MVC 3 (NET 4)での非同期について説明してくれる人はいないのだろうか。

編集

私はこの下の文書を何百回となく読み、大筋は理解したのですが、あまりにも矛盾したコメントが多いので、未だに混乱しているのです。

ASP.NET MVCで非同期コントローラを使用する

編集する

以下のようなコントローラアクションがあるとします。 AsyncController とはいえ)。

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

このように、私はあるオペレーションを起動し、そのことを忘れています。そして、完了を待たずにすぐに戻ってくる。

この場合、threadpoolからスレッドを使用する必要があるのでしょうか?もしそうなら、それが完了した後、そのスレッドはどうなるのでしょうか?どうなんでしょう? GC が入ってきて、完了した直後に掃除してくれるのでしょうか?

編集する。

Darin さんの回答に対して、データベースと通信する非同期コードのサンプルを示します。

public class FooController : AsyncController {

    //EF 4.2 DbContext instance
    MyContext _context = new MyContext();

    public void IndexAsync() { 

        AsyncManager.OutstandingOperations.Increment(3);

        Task<IEnumerable<Foo>>.Factory.StartNew(() => { 

           return 
                _context.Foos;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foos"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<Bars>>.Factory.StartNew(() => { 

           return 
                _context.Bars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["bars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<FooBar>>.Factory.StartNew(() => { 

           return 
                _context.FooBars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foobars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });
    }

    public ViewResult IndexCompleted(
        IEnumerable<Foo> foos, 
        IEnumerable<Bar> bars,
        IEnumerable<FooBar> foobars) {

        //Do the regular stuff and return

    }
}

解決方法は?

ここに 秀逸な記事 ASP.NETの非同期処理(非同期コントローラは基本的にこれを表しています)をより理解するために一読することをお勧めします。

まず、標準的な同期型アクションを考えてみましょう。

public ActionResult Index()
{
    // some processing
    return View();
}

このアクションにリクエストがあると、スレッドプールからスレッドが引き出され、このアクションの本体がこのスレッドで実行されます。したがって、このアクション内部の処理が遅い場合、このスレッドを処理全体にわたってブロックすることになるので、このスレッドを他のリクエストの処理に再利用することはできません。リクエストの実行が終了すると、このスレッドはスレッドプールに戻されます。

では、非同期パターンの例を見てみましょう。

public void IndexAsync()
{
    // perform some processing
}

public ActionResult IndexCompleted(object result)
{
    return View();
}

Index アクションにリクエストが送られると、スレッドプールから スレッドが引き出され、ボディに IndexAsync メソッドが実行されます。このメソッド本体の実行が終了すると、スレッドはスレッドプールに戻される。次に、標準の AsyncManager.OutstandingOperations 非同期処理の完了を合図すると、スレッドプールから別のスレッドが引き出され、そのスレッドプールにある IndexCompleted アクションが実行され、その結果がクライアントにレンダリングされます。

つまり、このパターンでわかることは、1つのクライアントのHTTPリクエストを2つの異なるスレッドで実行することができるということです。

さて、面白いのは IndexAsync メソッドを使用します。このメソッドの中でブロックする操作を行うと、ワーカスレッドをブロックしてしまうので、非同期コントローラの目的を完全に無駄にしてしまいます (このアクションの本体は、スレッドプールから取得したスレッドで実行されることを覚えておいてください)。

では、いつになったら非同期コントローラを本当の意味で活用できるのでしょうか?

IMHOでは、I/O集中型の操作(データベースやリモートサービスへのネットワーク呼び出しなど)を行う場合に、最も大きな利益を得ることができると考えています。CPUに負荷のかかる操作の場合、非同期アクションはあまり利益をもたらさないでしょう。

では、なぜI/O集中型の操作でメリットが得られるのでしょうか。それは I/O 補完ポート . IOCPは、全体の操作の実行中にサーバー上のスレッドやリソースを消費しないので、非常に強力です。

どのように動作するのですか?

例えば、リモートのウェブページのコンテンツを WebClient.DownloadStringAsync メソッドを使用します。このメソッドを呼び出すと、オペレーティングシステム内にIOCPが登録され、即座にリターンします。リクエスト全体の処理中、あなたのサーバーではスレッドが消費されません。すべてはリモートサーバーで行われます。これには多くの時間がかかりますが、ワーカスレッドを危険にさらすことはないので気にすることはありません。レスポンスを受信すると、IOCP がシグナルされ、スレッドプールからスレッドが引き出され、コールバックはこのスレッドで実行されます。しかし、ご覧のように、このプロセス全体を通して、どのスレッドも独占していません。

FileStream.BeginRead や SqlCommand.BeginExecute などのメソッドも同様です。

複数のデータベース呼び出しを並列化する場合はどうでしょうか。例えば、同期コントローラのアクションで、4つのブロッキングデータベース呼び出しを順番に実行したとします。各データベース呼び出しに200msかかるとすると、コントローラアクションの実行におよそ800msかかることは容易に計算できます。

これらの呼び出しを順次実行する必要がない場合、並列化することでパフォーマンスを向上させることは可能でしょうか?

これが大きな疑問で、簡単に答えられるものではありません。イエスかもしれないし、ノーかもしれない。それは、データベースの呼び出しをどのように実装するかに完全に依存します。前述したように非同期コントローラとI/Oコンプリーションポートを使用すれば、ワーカスレッドを独占しないので、このコントローラアクションと他のアクションのパフォーマンスを向上させることができます。

一方、これらの実装が不適切な場合(スレッドプールのスレッドでデータベース呼び出しをブロックする)、基本的にこのアクションの合計実行時間はおよそ200msに低下しますが、4つのワーカスレッドを消費するので、他のリクエストのパフォーマンスが低下し、それらを処理するスレッドがプールにないために飢餓状態になる可能性があります。

このように、非同期コントローラは非常に難しいもので、もしアプリケーションに対して大規模なテストを行う準備ができていないのであれば、非同期コントローラを実装しないようにしましょう。例えば、標準的な同期コントローラのアクションがアプリケーションのボトルネックになっていることを確認した場合(もちろん、広範囲にわたる負荷テストと測定を行った後)です。

では、あなたの例を考えてみましょう。

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}

Index アクションのリクエストを受けると、その本体を実行するためにスレッドプールからスレッドが引き出されますが、その本体は、以下のメソッドを使用して新しいタスクをスケジュールするだけです。 TPL . そのため、アクションの実行は終了し、スレッドはスレッドプールに戻される。ただし TPL は、スレッドプールのスレッドを使用して処理を実行します。そのため、元のスレッドがスレッドプールに戻されたとしても、このプールから別のスレッドを引き出してタスク本体を実行したことになります。つまり、貴重なプールから2つのスレッドを危険にさらしているわけです。

では、次のように考えてみましょう。

public ViewResult Index() { 

    new Thread(() => { 
        //Do an advanced looging here which takes a while
    }).Start();

    return View();
}

この場合、手動でスレッドを生成しています。この場合、Index アクションの本体の実行に少し時間がかかるかもしれません (新しいスレッドを生成するのは、既存のプールからスレッドを引き出すよりもコストがかかるためです)。しかし、高度なロギング操作の実行は、プールの一部ではないスレッドで行われます。したがって、他のリクエストに対応するために空いているプールからのスレッドを危険にさらすことはありません。