WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Core MVC の非同期プログラミング

by WebSurfer 2021年1月2日 17:40

先の記事「ASP.NET MVC の非同期プログラミング」は .NET Framework 版の MVC5 アプリの話ですが、ASP.NET Core 3.1 MVC アプリでも同様なことを検証しましたのでその結果を書きます。

ASP.NET Core 3.1 MVC の非同期プログラミング

以下に (1) async / await を利用した非同期プログラミングで使用されるスレッドがどのようになるか、(2) Task.Result などを使った同期コードと非同期コードを混在させるとデッドロックは起きるのか、(3) Task.ConfigureAwait(Boolean) メソッドを付与するとどう変わるかついて書きます。

意外だったのは、.NET Framework 版と違って、Task.Result を使ってもデッドロックならないということででした。

なお、Kestrel の場合はどうなるかですが、Visual Studio を使って IIS Express (インプロセス ホスティング) と Kestrel を切り替えて両方の動作を確認しました。どちらも同じ結果となりました。

(1) 使用されるスレッド

ASP.NET で非同期プログラミングを行う目的は、スレッドプールにある限られた数のスレッドを有効利用し、スループットを向上するためです。なので非同期メソッドのチェーンの一番深いところにある await 前後でスレッドが切り替わるはずです。

それを確認するために以下のコードで試してみた結果が上の画像です。期待通り、TimeCosumingMethod メソッドの await 前後で ManagedThreadId が 27 から 25 に変わっています。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" +
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = await TimeCosumingMethod();

    // .NET Framework 版の MVC5 アプリでは上に代えて以下のようにすると
    // デッドロックになったが Core 3.1 版ではデッドロックにはならない
    //ViewBag.Id2 = TimeCosumingMethod().Result;

    ViewBag.Id3 = "終了時, ID=" +
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    await Task.Delay(3000);

    // ConfigureAwait(false) の付与は結果に関係なし。
    // デッドロックになる時は ConfigureAwait(false) が勝手に付与され、
    // デッドロックにならない時は付与しても無視されるような感じ
    //await Task.Delay(3000).
    //    ConfigureAwait(continueOnCapturedContext: false);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id +
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

(2) Task.Result の使用

先の記事「await と Task.Result によるデッドロック」で書いたように Task.Result を使った同期コードと非同期コードを混在させるとデッドロックは起きるのかということの確認です。

結果は下の画像の通り処理は無事完了し、デッドロックは起きなかったです。.NET Framework 版の MVC5 アプリで TimeCosumingMethod の await Task.Delay(3000); に .ConfigureAwait(false) を付与した場合と同じ結果です。すなわち ID(OUT) のみ ManagedThreadId が異なり他は同じになっています。

Task.Result の使用

Task.Result をどのように使ったかは上のコードの AsyncTest アクションメソッドのコメントを見てください。.NET Framework 版での検証と全く同じやり方ですです。

.NET Framework 版でデッドロックなる理由は: まず、TimeCosumingMethod().Result で 1 つの同期ブロックが待機中となる。呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Result プロパティでの待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックになる・・・ということで、Core 3.1 でも同じになると信じていたんですが、一体どうなっているのでしょう?

(3) ConfigureAwait の付与

AsyncTest アクションメソッドで Result を使うか否かで結果は上の画像のように変わりますが、上のコードの TimeCosumingMethod のコメントに書きましたように、ConfigureAwait(false) の付与はその結果に関係なかったです。

Core では AsyncTest アクションメソッドで Result を使ってデッドロックになる時は ConfigureAwait(false) が勝手に付与され、正しく await してデッドロックにならない時は ConfigureAwait(false) を付与しても無視されているような感じです。

.NET Framework 版とは話が大きく変ってきてしまうのですが、一体どうなっているのでしょう。どうも今までの知識は Core には役に立たないようで、また勉強しなければならないようです。でも、今はその気力がないです。(笑)

Tags: , , , , ,

CORE

ASP.NET MVC の非同期プログラミング

by WebSurfer 2020年10月4日 15:50

ASP.NET MVC アプリで async / await を利用した非同期プログラミングで (1) 使用されるスレッドがどのようになるか、(2) Task.Result などを使った同期コードと非同期コードを混在させるとデッドロックは起きるのか、(3) ConfigureAwait メソッドでデッドロックは回避できるのかについて書きます。(.NET Framework アプリの話です。ASP.NET Core 3.1 MVC アプリの検証結果は別の記事に書きました)

ASP.NET MVC の非同期プログラミング

ちなみに ASP.NET Web Forms アプリ用の HTTP ハンドラで async / await を使って非同期呼び出しをする話は先の記事「非同期 HTTP ハンドラ (2)」に書きましたので興味があればそちらを見てください。

(1) 使用されるスレッド

ASP.NET で非同期プログラミングを行う目的はスレッドプールにある限られた数のスレッドを有効利用しスループットを向上するためです。そこが UI の応答性の向上を目的としている Windows Forms のような GUI アプリと違うところです。

ASP.NET アプリでは Web サーバーがクライアントから要求を受けるとスレッドプールからスレッドを確保して要求を処置します。非同期操作をしなければ、要求を受けてから応答を返すまで最初に確保したスレッドを保持し続けます。

Web アプリでは、外部のデータベースや Web API などにアクセスしてデータを取得するということが多いと思いますが、それに時間がかかる場合は一旦使っていたスレッドはスレッドプールに戻し、データ取得後の処理はスレッドプールから新たにスレッドを取得して行うようにすればスレッドプールのスレッドの有効利用が可能です。

そのあたりの詳細は Microsoft のドキュメント「ASP.NET の非同期/待機の概要」に図解入りで説明されているので見てください。

非同期プログラミングを行うと await 前後で実際にスレッドは違うのかを ASP.NET MVC アプリで試した結果が上の画像です。そのコードは以下の通りです。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = await TimeCosumingMethod();

    ViewBag.Id3 = "終了時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    await Task.Delay(3000);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id + 
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

上の画像の ID の数字 (ManagedThreadId) を見てください。TimeCosumingMethod メソッドの await 前後で ManagedThreadId が 6 から 7 に変わっています。ちなみに、Windows Forms のような GUI アプリでは await 前後いずれも UI スレッドになり ManagedThreadId は変わりません。

ASP.NET でも await で待機するときに現在のコンテキストがキャプチャされ、await 完了後はキャプチャしたコンテキストで続きの処理が行われるのは GUI アプリと同様だそうですが、await 前後で同じになるようにしているのはスレッドではなく HttpContext だそうです。それは仕組み上当たり前&そうせざるを得ないと思います。

(2) Task.Result でデッドロック

先の記事「await と Task.Result によるデッドロック」で書いたような Task.Result を使った同期コードと非同期コードを混在させるとデッドロックは起きるでしょうか?

その記事にも少し書きましたが、上のコードの await TimeCosumingMethod() を TimeCosumingMethod().Result に代えるとデッドロックは起きます。そのメカニズムは以下のようなことであろうと思います。

まず、TimeCosumingMethod().Result で 1 つの同期ブロックが待機中となる。呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Result プロパティでの待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックになる。

(3) ConfigureAwait でデッドロック回避

先の記事「ConfigureAwait によるデッドロックの回避」で書いたように、await 完了後の同期処理を実行するのに、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、別のコンテキストで行えばデッドロックにはなりません。

ConfigureAwait メソッドの使用

以下のコードのように ConfigureAwait(false) を追加することにより、await 完了後の残り処理は、キャプチャしたコンテキストではなく、スレッドプールのコンテキストで処理されるのでデッドロックは回避でき、上の画像のとおり実行が完了します。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = TimeCosumingMethod().Result;

    ViewBag.Id3 = "終了時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    // ConfigureAwait(false) を追加するとデッドロックは回避できる
    await Task.Delay(3000).
        ConfigureAwait(continueOnCapturedContext: false);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id + 
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

ただし、スレッドは await 前後で同じになります。ということは、要求を受けた時に確保したスレッドを応答を返すまでずっと使い続けていたということで、スレッドの有効利用という ASP.NET の非同期の目的は果たせてないようです。

await 前後でスレッドが異なる場合は、await 前にキャプチャしたコンテキストを await 後でも使わないと HttpContext が渡せないが、continueOnCapturedContext: false ではそれができないので同じスレッドを使い続けざるを得ないということではないかと思います。

Tags: , , ,

MVC

ConfigureAwait によるデッドロックの回避

by WebSurfer 2020年9月21日 13:22

先の記事「await と Task.Result によるデッドロック」の続きです。この記事には、先の記事で述べたデッドロックを回避する方法を書きます。

(5) ConfigureAwait

Task.ConfigureAwait(Boolean) メソッドを使って、先の記事で述べた Task.Result プロパティを使ったコードにおけるデッドロックを回避することができます。(UI がフリーズするのは避けられませんが)

ConfigureAwait メソッドの使用

Microsoft のドキュメント「非同期プログラミングのベストプラクティス」に以下の説明があります。

"既定では、未完了の Task を待機するときは、現在のコンテキストがキャプチャされ、Task が完了するときのメソッドの再開に使用されます。 ・・・中略・・・ GUI アプリケーションと ASP.NET アプリケーションには、一度に実行するコードを 1 つのチャンクに限定する SynchronizationContext があります。await が完了するときは、キャプチャしたコンテキスト内で async メソッドの残りを実行しようとします。"

初めは Task.Result で待機しているスレッドを await 終了後に使おうとするからデッドロックが起こるのだろうと思っていましたが、それでは ASP.NET のケースが説明できません。ASP.NET で非同期にする目的はスレッドプールのスレッドの有効利用で、await 前までに使っていたスレッドはスレッドプールに戻し、await が完了後の処理はスレッドプールから新たにスレッドを取得して行いますので(ASP.NET の場合 await 前後でスレッドが異なるということ)。

少なくとも ASP.NET の場合、使われるスレッドが await 前後で同じかどうかはデッドロックに関係なく、上に書いた「一度に実行するコードを 1 つのチャンクに限定する」が関係しているようです。

ということは、GUI アプリ / ASP.NET アプリどちらにせよ、await が完了した後の残りの処理を、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、別のコンテキストで行えばデッドロックにはならないはずです。

Microsoft のドキュメント「非同期プログラミングのベストプラクティス」に以下のように書いてあります。

"ConfigureAwait(continueOnCapturedContext: false) を追加すると、デッドロックが回避されます。この場合、await が完了するとき、スレッドプールのコンテキスト内で async メソッドの残り処理の実行が試みられます。このメソッドは完了でき、返されたタスクを完了するため、デッドロックは発生しません。"

というわけで、ConfigureAwait メソッドを使って以下のコードで試してみました。上の画像を表示したものです。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsAsyncTest
{
    public partial class Form3 : Form
    {
        public Form3()
        {
            InitializeComponent();
        }

        // Task.Result 使用
        private void button1_Click(object sender, EventArgs e)
        {
            label1.Text = "UI スレッド、ID=" + 
                Thread.CurrentThread.ManagedThreadId;

            label2.Text = TimeCosumingMethod().Result;

            label1.Text += " / ID=" +
                Thread.CurrentThread.ManagedThreadId;
        }

        // await 使用
        private async void button2_Click(object sender, EventArgs e)
        {
            label1.Text = "UI スレッド、ID=" +
                Thread.CurrentThread.ManagedThreadId;

            label2.Text = await TimeCosumingMethod();

            label1.Text += " / ID=" +
                Thread.CurrentThread.ManagedThreadId;
        }

        private async Task<string> TimeCosumingMethod()
        {
            await Task.Delay(3000).ConfigureAwait(
                continueOnCapturedContext: false);

            return "TimeCosumingMethod の戻り値、ID=" +
                Thread.CurrentThread.ManagedThreadId;
        }
    }
}

上のコードのように ConfigureAwait(continueOnCapturedContext: false) を追加すると、TimeCosumingMethod メソッドで await Task.Delay(3000) が完了した後の残りの処理は、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、スレッドプールのコンテキスト内で実行されるためデッドロックは回避できるということのようです。

上の画像を見てください。UI スレッドの ManagedThreadId(上の画像では ID=1)と TimeCosumingMethod メソッドの残りを実行したスレッド(上の画像では ID=4)が異なっています。

ただし、Task.Result を使う方は UI がフリーズするのは避けられません。一方、await を使う方は UI はフリーズしません(メッセージループは常に動いていて、マウスのクリックなどのユーザーイベントを処理して UI に反映してくれます)。

理由は、Task.Result は UI スレッドをブロックして TimeCosumingMethod メソッドが完了するのを同期的に待機するのに対して、await は呼び出し元に処理を戻して非同期的に待機するからのようです。

@IT の記事「第2回 非同期メソッドの構文」に以下の説明があります。

"await という名前から、「渡した非同期タスクが完了するまで、呼び出し元スレッドをブロックして待機する」というように感じるかもしれないが、実際はそうではない。await 演算子の意味は、「待っているタスクがまだ完了していない場合、メソッドの残りをそのタスクの『継続』として登録して呼び出し元に処理を戻し、タスクが完了したら登録しておいた継続処理を実行すること」なので注意が必要だ。"

Windows Forms アプリでは「Application.Run メソッド」によって UI スレッドで標準のメッセージループの実行が開始されるそうです。

なので、Task.Result を使う方は、UI スレッドがブロックされている間(上のコード例では約 3 秒間)はメッセージループは動いておらず、マウスのクリックなどのユーザーイベントには無反応(UI がフリーズ状態)になるということのようです。

Tags: , , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar