【ASP.NET Core】Singletonなアンマネージリソースの解放【DIコンテナ】

こんにちは、サイオステクノロジーの佐藤 陽です。
今回は .NET におけるアンマネージリソースの解放についてお話ししていきたいと思います。

AzureのClientのリソース解放などについて言及していくので
そういったクライアントリソースの扱いが気になった方も最後までご覧ください!

はじめに

アンマネージリソースは使った後にリソースを解放する必要があります。

多くのアンマネージリソースはIDisposable/IDisposableAsyncのインターフェースを実装してあるため
usingを使って実装することで、usingのブロックを抜けたタイミングでリソースが破棄され、自動的にGCによってメモリが解放されます。

例:

string connectionString = "<connection_string>";
await using var client = new ServiceBusClient(connectionString);

シングルトンのリソース破棄

ただ、リソースをSingletonとして所持している場合はどうでしょうか?

例えば、AzureのServiceBusClientのドキュメントを読むと

The ServiceBusClient, senders, receivers, and processors are safe to cache and use as a singleton for the lifetime of the application, which is best practice when messages are being sent or received regularly. They are responsible for efficient management of network, CPU, and memory use, working to keep usage low during periods of inactivity.

という記載があり、定期的に使う場合はSingletonとして利用することがベストプラクティスであると記載があります。

ServiceBusのを利用するクラスを実装する場合、以下のような実装が想定されます。
このクラスをSingletonとして所持することで、serviceBusClientを効率よく使う事ができますね。

namespace SIOS.AzureClient
{
    public class AzureQueueClient
    {
        private readonly ServiceBusClient serviceBusClient;
        private readonly ServiceBusSender serviceBusSender;

        public AzureQueueClient()
        {
            serviceBusClient = new("serviceBusNamespace", new DefaultAzureCredential());
            serviceBusSender = serviceBusClient.CreateSender("queueName");
        }

        public async Task Enqueue(string message)
        {
            ServiceBusMessage serviceBusMessage = new(message);
            await serviceBusSender.SendMessageAsync(serviceBusMessage);
        }
    }
}

ただこういった実装の場合、usingステートメントが扱えません。
そのため自分で明示的にserviceBusClientのDispose()を呼び出してあげる必要があります。

破棄してみる

今回、大きな前提となるのが、ASP.NET CoreのDIコンテナを使っており、AddSingletonでInjectionしていることです。
この前提を満たす・満たさないで実装方法大きく変わってくるので注意してください。

では、DIコンテナを使っていると何がいいのかというと、ドキュメントの色々な部分に書いてあるのですが
シングルトンのリソースを使い終わったタイミングで、自動でDIコンテナが責任を持ってリソースを破棄してくれるようです。

DisposeAsync メソッドの実装

依存関係の挿入に関して、サービスを IServiceCollection に登録すると、IServiceCollectionがお客様に代り暗黙的に管理されます。 IServiceProvider とそれに対応する IHost によって、リソースのクリーンアップが調整されます。 具体的には、IDisposable および IAsyncDisposable の実装は、それらに指定した有効期間の終了時に適切に破棄されます。

.NET 依存関係の挿入

依存関係の挿入コンテナーから送信されるサービス実装の後続の要求すべてには、同じインスタンスが使用されます。 アプリをシングルトンで動作させる必要がある場合は、サービス コンテナーによるサービスの有効期間の管理を許可してください。 シングルトン デザイン パターンを実装したり、シングルトンを破棄するコードを提供したりしないでください。 コンテナーからサービスを解決したコードによって、サービスが破棄されることはありません。 型またはファクトリがシングルトンとして登録されている場合、コンテナーによってシングルトンが自動的に破棄されます。

依存関係の挿入のガイドライン

コンテナーで作成された型のクリーンアップはコンテナーによって行われ、IDisposable インスタンスで Dispose が呼び出されます。 コンテナーから解決されたサービスが、開発者によって破棄されることはありません。 型またはファクトリがシングルトンとして登録されている場合、コンテナーによってシングルトンが自動的に破棄されます。

実装

ではどう実装していくかを見ていきたいと思います。

先ほどは省略して書きましたが、DI想定のソースコードだと

namespace SIOS.AzureClient
{
    public class AzureQueueClient : IQueueClient
    {
        private readonly ServiceBusClient serviceBusClient;
        private readonly ServiceBusSender serviceBusSender;

        public AzureQueueClient(IOptions<AzureOptions> options)
        {
            serviceBusClient = new("serviceBusNamespace", new DefaultAzureCredential());
            serviceBusSender = serviceBusClient.CreateSender("queueName");
        }

        public async Task Enqueue(string message)
        {
            ServiceBusMessage serviceBusMessage = new(message);
            await serviceBusSender.SendMessageAsync(serviceBusMessage);
        }
    }
}

といって、Interfaceを実装した形となり
Startup.csにおいて以下のようにSingletonとしてInjectionします。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IQueueClient, AzureQueueClient>();
}

こうすることで、アプリケーション終了時にAzureQueueClientのDisposeが呼ばれるようになります。

よって、AzureQueueClientのDisposeメソッドの中で、ServiceBusClientのDisposeを呼び出してあげればリソースの破棄が行えますね。

AzureQueueClientに実装を加えていきます。
変更点としては2点です。

  1. AzureQueueClientに対して、IAsyncDisposableをImplementすること
  2. DisposeAsyncメソッドを実装し、内部でserviceBusClientのDisposeAsyncを呼び出すこと

なおドキュメントにもある通り、ServiceBusSenderはServiceBusClient破棄される際に自動的に破棄されるため、処理を除外しています。

public class AzureQueueClient : IQueueClient, IAsyncDisposable
{
    private readonly ServiceBusClient serviceBusClient;
    private readonly ServiceBusSender serviceBusSender;


    public AzureQueueClient(IOptions<AzureOptions> options)
    {
        serviceBusClient = new(options.Value.ServiceBusNameSpace, new DefaultAzureCredential());
        serviceBusSender = serviceBusClient.CreateSender(options.Value.ServiceBusQueue);
    }

    //ココ↓
    public async ValueTask DisposeAsync()
    {
        if (serviceBusClient?.IsClosed == false)
        {
            await serviceBusClient.DisposeAsync();
        }
    }


    public async Task Enqueue(string message)
    {
        ServiceBusMessage serviceBusMessage = new(message);
        await serviceBusSender.SendMessageAsync(serviceBusMessage);
    }
}

動作確認

さて動作確認しよう!

というところで「ローカルでアプリケーション終了時の動作確認ってどうやってやるんだ…?」となりました。

少し調べたところ

  1. ローカルでアプリを起動
  2. Windowsのタスクバー右下の[ヘ]みたいなアイコンから、隠れているインジケーターを表示
  3. IIS Expressのアイコンがあるので右クリック
  4. 対象のサイトを選択して「サイトの停止」

の手順で出来ました。

なおこれは恐らくPCの環境や、.NETのプロジェクト構成にも依って異なってくるかと思うので参考までに。

実際に行うと、上記の実装でServiceBusClientのDisposeAsync()が呼び出されることが確認できました。

DIじゃないときは?

今回は.NETのDIコンテナを使っており、かつAddSingletonでInjectionしている場合のケースをご紹介しました。

それ以外の場合はどうなるのか?というと、もちろん方法は色々あるかと思います。
ただ今回は少し紹介すると長くなるので、機会があれば別の記事でご紹介したいと思います。

まとめ

今回は、アンマネージリソースをSingletonとして保持している場合に、リソースを破棄する方法についてご紹介しました。

.NETのDIコンテナを使い、AddSingletonで追加した場合は、コンテナ側でアプリケーション終了時に破棄してくれるため、そのタイミングで併せて破棄すればOKでした。楽ちんですね。
使い終わったものはしっかり破棄していくようにていきしましょう!

ではまた!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です