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

.NET Core での Dependency Injection

by WebSurfer 2021年1月1日 14:35

.NET Core アプリでは Microsoft の Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類を使って Dependency Injection (DI) 機能を実装できるという話を書きます。

NuGet でインストール

Visual Studio 2019 のテンプレートで ASP.NET Core MVC / Razor Page アプリを作成すると DI コンテナを含めて DI に必要な機能がフレームワークに組み込まれ、要求を受けて ASP.NET が Controller を初期化する際、Controller が依存するクラスのインスタンスが自動的に生成され、そのインスタンスへの参照が Controller のコンストラクタの引数に渡されるようになっています。(詳しくは Microsoft のドキュメント「ASP.NET Core での依存関係の挿入」を見てください)

その際に使われているのが Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類だそうです。なので、ASP.NET Core アプリでなくても、例えば .NET Core コンソールアプリでも、上の画像のように NuGet で必要なアセンブリをプロジェクトにインストールすれば DI 機能を実装できます。

知ってました? 実は無知な自分は最近まで知らなかったです。(汗) 試しに .NET 5.0 ベースのコンソールアプリに DI ���能を実装してみましたので、備忘録として書いておくことにした次第です。

サードパーティ製の DI コンテナは巷に多々あるそうで、自分も Simple Injector を使ってみたことがあります。(その話は「SimpleInjector を ASP.NET MVC & Web API で利用」に書きましたので興味があれば見てください)

Simple Injector なら多少の実装経験があるということで、比較のために Simple Injector のドキュメント Quick Start にあるサンプルとほぼ同じコンソールアプリを、Microsoft.Extensions.DependencyInjection 名前空間の ServiceCollection クラスServiceProvider クラスを利用して実装してみました。

そのコードは以下の通りです。サービスに DI するクラスを登録するのに AddTransient, AddScoped, AddSingleton の 3 種類を使ってますが、使い分けているわけではなくて、このようなコンソールアプリではどれを使っても同じで、試しに全種類を使ってみただけですのでご注意ください。違いはこの記事の下の方で説明します。

using System;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleAppDependencyInjection
{
    class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();

            services.AddTransient<IOrderRepository, SqlOrderRepository>();
            services.AddSingleton<ILogger, Logger>();
            services.AddScoped<IEventPublisher, EventPublisher>();
            services.AddTransient<CancelOrderHandler>();

            var provider = services.BuildServiceProvider();
            var handler = provider.GetRequiredService<CancelOrderHandler>();

            var orderId = Guid.NewGuid();
            var command = new Order { OrderId = orderId };
            handler.Handle(command);
        }
    }

    public class CancelOrderHandler
    {
        private readonly IOrderRepository repository;
        private readonly ILogger logger;
        private readonly IEventPublisher publisher;

        // Use constructor injection for the dependencies
        public CancelOrderHandler(IOrderRepository repository, 
                                  ILogger logger, 
                                  IEventPublisher publisher)
        {
            this.repository = repository;
            this.logger = logger;
            this.publisher = publisher;
        }

        public void Handle(Order command)
        {
            this.logger.Log($"Cancelling order {command.OrderId}");
            var order = this.repository.GetById(command.OrderId);
            order.OrderStatus = "Cancelled";
            this.repository.Save(order);
            this.publisher.Publish(order);
        }
    }

    public interface IOrderRepository
    {
        public Order GetById(Guid orderId);

        public void Save(Order order);
    }

    public class SqlOrderRepository : IOrderRepository
    {
        private readonly ILogger logger;

        // Use constructor injection for the dependencies
        public SqlOrderRepository(ILogger logger)
        {
            this.logger = logger;
        }

        public Order GetById(Guid orderId)
        {
            this.logger.Log($"Getting Order {orderId}");

            // Retrieve from db.・・・のつもり
            var order = new Order
            {
                OrderId = orderId,
                ProductName = "911-GT3",
                OrderStatus = "Ordered"
            };

            return order;
        }

        public void Save(Order order)
        {
            this.logger.Log($"Saving order {order.OrderId}");
            // Save to db.
        }
    }

    public interface ILogger
    {
        void Log(string log);
    }

    public class Logger : ILogger
    {
        public void Log(string log)
        {
            Console.WriteLine(log);
        }
    }

    public interface IEventPublisher
    {
        public void Publish(Order order);
    }

    public class EventPublisher : IEventPublisher
    {
        public void Publish(Order order)
        {
            Console.WriteLine($"Publish order {order.OrderId}, " +
                $"{order.ProductName}, {order.OrderStatus}");
        }
    }

    public class Order
    {
        public Guid OrderId { get; set; }

        public string ProductName { get; set; }

        public string OrderStatus { get; set; }
    }
}

実行結果は以下の画像のようになります。

実行結果

上のコードでサービスに DI するクラスを登録する AddSingleton, AddScoped, AddTransient メソッドの違いは、Microsoft.Extensions.DependencyInjection Deep Dive という記事の「生成と破棄」のセクションに詳しく書いてあります。

上の記事の説明では違いが自分には分かりませんでしたが、ASP.NET Web アプリで説明すると以下のようになるようです。

  • AddSingleton: 一度 DI されると、アプリケーションが終了するまで、最初の DI で生成されたインスタンスを使いまわす
  • AddScoped: 一つの HTTP 要求から応答を返すまでの間では、最初の DI で生成されたインスタンスをその後の DI でも使いまわす
  • AddTransient: DI が行われるたびに新しいインスタンスを生成する

ASP.NET アプリはクライアントから要求を受けるたびにスレッドプールからスレッドを取得し、処理に必要なアセンブリをメモリにロードし、処理が完了してクライアントに応答を返すとメモリをクリアし、使ったスレッドをプールに戻すマルチスレッドアプリです。(ググって調べた記事 Is Kestrel using a single thread for processing requests like Node.js? などを見た限りですが Kestrel も同じだそうです。)

AddSingleton を選んだ場合、ワーカープロセスが立ち上がった後の最初の DI でインスタンスが生成されると、ワーカープロセスがリサイクルされるまで、すべての要求に同じインスタンスが使い回されるということになります。初期化するのに時間がかかるとかメモリなどリソースを大量に消費するクラスを DI する場合には AddSingleton の利用を考えるのがよさそうです。

AddScoped の使い道、即ち一回の要求から応答を返すまでの間に同じクラスを複数回 DI するケースというのは、View への DI もサポートされていること(詳しくは「ASP.NET Core でのビューへの依存関係の挿入」参照)、サービス、ミドルウェアその他カスタムクラスにも DI 機能を実装することができることを考えるといろいろありそうです。

例えば、Visual Studio で作成したプロジェクトでは、ASP.NET Identity を利用した認証関係の Razor Class Library (RCL) のページモデルと _LoginPartial.cshtml では、SignInManager<IdentityUser>, UserManager<IdentityUser> を DI する設定になっています。そういうクラスは AddScoped で ServiceCollection に登録しておくのが良さそうです。

AddTransient を使うと、Controller、View、サービス、ミドルウェアその他カスタムクラスで DI 操作が行われるたび、新たにインスタンスを生成してそれへの参照を渡すということになります。Account confirmation and password recovery in ASP.NET Core に書いてあった EmailSender クラスの登録などで例を見ました。(実際に AddTransient を使う必要があるのかは分かりませんが)

Tags: , ,

CORE

ASP.NET と HttpClient (CORE)

by WebSurfer 2020年11月8日 14:52

ASP.NET Core 3.1 MVC アプリから HttpClient を利用して他のサイトの ASP.NET Web API にアクセスして情報を取得する方法を書きます。

ASP.NET と HttpClient

HttpClient のインスタンスを生成すると、そのたびにソケットも生成されます。しかし、HttpClient のインスタンスを Dispose してもソケットはクローズされないので(下記注参照)、何度も繰り返すとソケットの枯渇につながるという問題があり、それを避けるため、HttpClient のインスタンスはシングルトンにしてアプリで使いまわすということを行うそうです。ただし、そうすると DNS の変更が反映されないという別の問題があるそうですが。

注: Microsoft のドキュメント「ASP.NET Core パフォーマンスのベストプラクティス」によると "Closed HttpClient instances leave sockets open in the TIME_WAIT state for a short period of time" とのことです。別の記事「開発者を苦しめる.NETのHttpClientのバグと紛らわしいドキュメント」にはデフォルトは 4 分と書いてあります。

.NET Framework 版の ASP.NET Web アプリでの対処方法は Microsoft のドキュメント「Improper Instantiation antipattern」の How to fix the problem というセクションに書かれているのを見つけました。

そのドキュメントには、コントローラーに、

private static readonly HttpClient httpClient;

という static フィールドを設けて、コントローラーのコンストラクタで、

httpClient = new HttpClient();

とすると書いてあります。しかし、コントローラーのコンストラクタはクライアントから要求を受けるたびに呼び出されるので、要求を受けるたびに HttpClient のインスタンスを新たに作るということになってしまうと思うのですが・・・ 無知な自分には何故それが問題ないのか理解し難いです。

でも、まぁ、Microsoft のドキュメントですし、検証したようですし、.NET Framework 版の ASP.NET アプリでは他に適当な手はなさそうですし、もし問題が起きたら Microsoft のせいにできるので(笑)、その方法を使ってみるのが良いかもしれません。

しかし、Core 2.1 以降の ASP.NET Web アプリでは話が違ってくるようで、Microsoft の以下のドキュメントに書いてある IHttpClientFactory を利用する手段があるそうです。

詳しい仕組みの理解はちょっと置いといて、要するに上の一番目の記事の IHttpClientFactory の代替手段のセクションに書いてある以下の点を信じればよさそうです。(翻訳がイマイチなので英語版)

Using IHttpClientFactory in a DI-enabled app avoids:

  • Resource exhaustion problems by pooling HttpMessageHandler instances.
  • Stale DNS problems by cycling HttpMessageHandler instances at regular intervals.

上の 2 つの問題の前者は HttpClient のインスタンスの生成・廃棄を繰り返すことによるソケットの枯渇、後者はそれに対処するためシングルトンにして長期に使いまわすと DNS の変更が反映されないことを言っており、Core に備わっている DI 機能を使って IHttpClientFactory オブジェクトを注入する方法でそれらの問題を回避できるということのようです。

というわけで、詳しい仕組みは理解できてませんが、とりあえず���の一番目の記事の「基本的な使用方法」のセクションに従って実装してみました。

Startup.cs

namespace MvcCoreApp
{
    public class Startup
    {        
        // ・・・中略・・・

        public void ConfigureServices(IServiceCollection services)
        {
            // 以下を追加。これにより IHttpClientFactory を DI できる
            services.AddHttpClient();

        // ・・・中略・・・
}

Controller / Action Method

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;
using System.IO;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;

namespace MvcCoreApp.Controllers
{
    public class IHttpClientFactoryController : Controller
    {
        private readonly IHttpClientFactory _clientFactory;

        public IHttpClientFactoryController(
                              IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<IActionResult> Index()
        {
            var request = new HttpRequestMessage(HttpMethod.Get, 
                                    "https://localhost:44365/values");
            HttpClient client = _clientFactory.CreateClient();
            HttpResponseMessage response = await client.SendAsync(request);
            List<Hero> list = null;

            if (response.IsSuccessStatusCode)
            {
                using (Stream responseStream = 
                              await response.Content.ReadAsStreamAsync())
                {
                    list = await System.Text.Json.JsonSerializer.
                           DeserializeAsync<List<Hero>>(responseStream);
                }
            }

            // JSON 文字列のエスケープ回避&インデント設定
            return Json(list, new JsonSerializerOptions
            {
                Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
                WriteIndented = true,
            });
        }
    }

    public class Hero
    {
        public int id { get; set; }
        public string name { get; set; }
    }
}

上のコントローラーの Index アクションメソッドを呼び出した結果がこの記事の上の方にある画像です。一応動くということを確認しただけで、ソケットの枯渇とか DNS の変更に対応できているかは分かりませんが。 (汗)

最後にもう一つ。ASP.NET Core 3.1 Web API が返す JSON 文字列のキーの最初の文字が小文字になってしまうことに注意してください。Web API でも同様で、デフォルトで camel casing になるということだそうです。なので、上の Hero クラスのプロパティの最初の文字を小文字にしておかないとデシリアライズに失敗します。camel casing になるのを回避する方法はあります。詳しくは別の記事「JsonSerializer の Camel Casing」を見てください。

Tags: , ,

CORE

About this blog

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

Calendar

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

View posts in large calendar