WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

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

by WebSurfer 2. January 2021 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 1. January 2021 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

タスク並列ライブラリ (TPL)

by WebSurfer 27. December 2020 14:53

タスク並列ライブラリ (TPL) は Windows Forms のような GUI アプリでは使い難い点があるということを今更ながらですが学びましたので、備忘録として書いておきます。

タスク並列ライブラリの検証

最近の PC の CPU はマルチコアが当たり前になっているようですが、PC のマルチコアを有効に使うプログラミングをサポートするため、.NET Framework 4.0 で導入されたタスク並列ライブラリ (TPL・・・Parallel.For とか Parallel.Invoke とか) を利用するという話を巷でよく耳にします。

マルチスレッドの基本的な話ですが、@IT の記事「第1回 マルチスレッドはこんなときに使う (1/2)」の「マルチスレッドの動作原理」セクションを見てください。リンク切れになると困るので画像だけ借用して以下に表示しておきます。

マルチスレッドの比較

② のようになると OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅くなるということになります。なので、マルチスレッドアプリで処置時間の短縮を図るなら ③ のようにマルチコアを利用できる環境が必要で、さらにマルチコアを有効に利用するプログラミングを行うという話になると思います。

その際に自分がよく聞くのがタスク並列ライブラリ (TPL) や Parallel LINQ (PLINQ) を使うという話です。Microsoft のドキュメント「.NET での並列プログラミング」や、ググるとヒットする記事例えば @IT の記事「ループをParallelクラスで並列処理にするには?」を読むと TPL, PLINQ は並列処理には万能のような気がしてました。

Microsoft のドキュメント「タスク並列ライブラリ (TPL)」には、

"TPL は、使用可能なすべてのプロセッサを最も効率的に使用するように、コンカレンシーの程度を動的に拡大します。The TPL scales the degree of concurrency dynamically to most efficiently use all the processors that are available."

・・・と言う記述がありますし、PC のマルチコアを有効に使うという局面に限れば最強のように思えます。

しかし、Windows Forms のような GUI アプリではそうでもなさそうな感じです。自分が気が付いた限りですが、以下の 2 点が問題だと思いました。(自分が回避策を知らないだけという可能性は否定できませんが)

  1. 並列に実行される複数のメソッドがすべて完了するまで UI がブロックされる。
  2. 並列実行するメソッドには非同期版は使えない。

以下のサンプルコードの ParallelInvoke_Click メソッドは Parallel.Invoke を使って 5 つの同期版メソッド Work を並列に実行するものですが、5 つのメソッドがすべて完了するまで UI がブロックされるので、アプリはフリーズしたようになります。その他の TPL, PLINK を使った xxxxx_Click メソッドも同様で、完了するまで UI がブロックされます。それを回避する手段は、自分が探した限りですが、なさそうです。

さらに、非同期版メソッド WorkAsync は使えません。詳しくはサンプルコードのコメントに書きましたので見てください。なので、ライブラリなどで非同期版メソッドしか提供されてない場合は何ともならないと思われます。

というわけで、下のサンプルコードの WhenAll1_Click, WhenAll2_Click メソッドのように、Parallel.Invoke など使わないで、await Task.WhenAll(...) で待機するようにし、並列化については OS に任せるのが良さそうと思いました。(実際にサンプルを動かしてみると並列化してくれているような感じはしました。② のようになっている可能性は否定しきれませんが)

非同期版メソッド WorkAsync も使えます。下のサンプルコードの WhenAll2_Click メソッドを見てください。上の画像がその実行結果です。(ThreadID が 1 で UI スレッドと同じなのは、WorkAsync メソッドの中で ManagedThreadId を取得するのが UI スレッドだからです)

using System;
using System.Windows.Forms;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace WinFormsApp1
{
    public partial class TaskParallelLibrary : Form
    {
        public TaskParallelLibrary()
        {
            InitializeComponent();
        }

        // 同期版メソッド
        private string Work(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start:{DateTime.Now:ss.fff}, ";

            Thread.Sleep(3000);

            retunVlaue += $"end:{DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド
        private async Task<string> WorkAsync(int n)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            string retunVlaue =
                $"n = {n}, ThreadID = {id}, start: {DateTime.Now:ss.fff}, ";

            await Task.Delay(3000);

            retunVlaue += $"end: {DateTime.Now:ss.fff}";

            return retunVlaue;
        }

        // 非同期版メソッド 5 つを for ループで逐次実行
        private async void InOrder_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            for (int n = 0; n < 5; n++)
            {
                this.label1.Text += await WorkAsync(n) + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを Parallel.Invoke で実行
        // 終了まで UI はブロックされる
        private void ParallelInvoke_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.Invoke(
                () => results[0] = Work(0),
                () => results[1] = Work(1),
                () => results[2] = Work(2),
                () => results[3] = Work(3),
                () => results[4] = Work(4));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";

            // 非同期版メソッド WorkAsync は使えない

            // 以下のようにすると Task.Result でデッドロックになる
            //Task<string> result1 = null, result2 = null;
            //Parallel.Invoke(
            //    () => result1 = WorkAsync(0),
            //    () => result2 = WorkAsync(1));
            //this.label1.Text += result1.Result + "\r\n";
            //this.label1.Text += result2.Result + "\r\n";

            // 以下のようにすると await で待つことなく終わってしまう
            //string result1 = "", result2 = "";
            //Parallel.Invoke(
            //    async () => result1 = await WorkAsync(0),
            //    async () => result2 = await WorkAsync(1));
            //this.label1.Text += result1 + "\r\n";
            //this.label1.Text += result2 + "\r\n";

        }

        // 非同期版メソッド 5 つを Parallel.For で実行
        // 終了まで UI はブロックされる
        private void ParallelFor_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.For(0, 5, n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを Parallel.ForEach で実行
        // 終了まで UI はブロックされる
        private void ParallelForEach_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];

            Parallel.ForEach(Enumerable.Range(0, 5), 
                             n => results[n] = Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを PLINK で実行
        // 終了まで UI はブロックされる
        private void PLINK_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            var results = Enumerable.Range(0, 5).AsParallel().
                          Select(n => Work(n));

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // ***** 以下 TPL, PLINK に代えて Task.WhenAll を使用 *****

        // 同期版メソッド 5 つを Task.Run で実行、
        // await Task.WhenAll で待機        
        private async void WhenAll1_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            string[] results = new string[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(Task.Run(() => results[i] = Work(i)));
            }

            // WaitAll は await で待機できないので注意
            await Task.WhenAll(taskList.ToArray());

            foreach (string result in results)
            {
                this.label1.Text += result + "\r\n";
            }

            this.label1.Text += "完了";
        }

        // 非同期版メソッド 5 つを実行、
        // await Task.WhenAll で待機
        private async void WhenAll2_Click(object sender, EventArgs e)
        {
            int id = Thread.CurrentThread.ManagedThreadId;
            this.label1.Text = $"UI Thread ID = {id}\r\n";

            Task<string>[] results = new Task<string>[5];
            var taskList = new List<Task>();
            for (int n = 0; n < 5; n++)
            {
                int i = n;
                taskList.Add(results[i] = WorkAsync(i));
            }

            await Task.WhenAll(taskList.ToArray());

            foreach (Task<string> result in results)
            {
                this.label1.Text += result.Result + "\r\n";
            }

            this.label1.Text += "完了";
        }

    }
}

Tags: , , , , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  January 2021  >>
MoTuWeThFrSaSu
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar