web-dev-qa-db-ja.com

ASP.NETCoreで「ファイアアンドフォーゲット」メソッドを実装する安全な方法

複数のプロジェクトで使用される単純なロギングライブラリを実装しようとしています。ライブラリの仕事は、HTTPリクエストをElasticSearchに送信することです。このライブラリの主なポイントは、応答を待ってはならないということです。また、エラーや例外については気にしません。 ElasticSearchにリクエストを送信し、すぐに戻る必要があります。戻り値の型がTaskのインターフェースを作成したくないので、voidのままにしておきます。

以下は私のサンプルコードです。それは「ファイアアンドフォーゲット」の正しく安全な実装ですか?高負荷ライブラリでTask.Run()を使用しても大丈夫ですか?または、私の場合はTask.Run()の使用を避ける必要がありますか?また、awaitTask.Run()と一緒に使用しない場合、スレッドをブロックしますか?このコードはライブラリにあります:

public enum LogLevel
{
    Trace = 1,
    Debug = 2,
    Info = 3,
    Warn = 4,
    Error = 5,
    Fatal = 6
}

public interface ILogger
{
    void Info(string action, string message);
}

public class Logger : ILogger
{
    private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false });
    private static IConfigurationRoot _configuration;

    public Logger(IConfigurationRoot configuration)
    {
        _configuration = configuration;
    }

    public void Info(string action, string message)
    {
        Task.Run(() => Post(action, message, LogLevel.Info));
        /*Post(action, message, LogLevel.Info);*/ // Or should I just use it like this?
    }

    private async Task Post(string action, string message, LogLevel logLevel)
    {
        // Here I have some logic

        var jsonData = JsonConvert.SerializeObject(log);
        var content = new StringContent(jsonData, Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync(_configuration.GetValue<string>("ElasticLogger:Url"), content);
        // No work here, the end of the method
    }
}

これは、WebAPIのStartupクラスのConfigureServicesメソッド内にロガーを登録する方法です。

public void ConfigureServices(IServiceCollection services)
{
     // ......

     services.AddSingleton<ILogger, Logger>();

     // .....
}

このコードは私のWebAPI内のメソッドにあります:

public void ExecuteOperation(ExecOperationRequest request)
{
    // Here some business logic

    _logger.Info("ExecuteOperation", "START"); // Log

   // Here also some business logic

    _logger.Info("ExecuteOperation", "END"); // Log
}
9
Nomad

Re:非同期メソッドへの待機なしの呼び出しとTask.Run()

PostにはCPUバウンド作業が少量しかないため(つまり、jsonペイロードを作成する)、別の_Task.Run_のメリットはありません。スレッドプールで新しいタスクをスケジュールするオーバーヘッドは、IMOのメリットを上回ります。 。つまり.

_Post(action, message, LogLevel.Info);*/ // Or should I just use it like this?
_

2つのアプローチの中で優れています。待機していないタスク内に関連付けられているコンパイラの警告を抑制し、次の開発者がコードに出くわすためにコメントを残したい場合があります。

しかし、Stephen Clearyの決定的な答えによると、ファイアアンドフォーゲット ASP.Netはほとんど良い考えではありません 。作業をオフロードすることをお勧めします。キューを介して、Windowsサービス、Azure WebJobなどに。

追加の危険性があります-待機していないタスクがスローされた場合は、 例外を観察してください

また、Postの後に行われる作業(たとえば、responseを使用する場合)は、これが継続タスクであり、スレッドプールでスケジュールする必要があることに注意してください-大量に起動する場合Postメソッドの場合、完了するとスレッドの競合が多く発生します。

Re:また、Task.Run()でawaitを使用しない場合、スレッドをブロックしますか?

awaitスレッドは必要ありませんawaitは、コードを非同期で書き直すようコンパイラーに要求する構文糖衣です。 Task.Run()は、ThreadPoolで2番目のタスクをスケジュールします。このタスクは、PostAsyncメソッドに到達する前にごくわずかな作業しか実行しないため、使用しないことをお勧めします。

InfoからPostへの待機中の呼び出しでの呼び出し元スレッドの使用/ブロックの量は、Taskが返される前に実行される作業の種類によって異なります。あなたの場合、Jsonのシリアル化作業は呼び出し元のスレッドで実行されます(私は#1とラベルを付けました)が、実行時間はHTTP呼び出し時間と比較して無視できるはずです。したがって、メソッドInfoで待機することはありませんが、HTTP呼び出しの後のコードは、Http呼び出しの完了時にスケジュールする必要があり、使用可能なスレッド(#2)でスケジュールされます。

_public void Info(string action, string message)
{
#pragma warning disable 4014 // Deliberate fire and forget
    Post(action, message, LogLevel.Info); // Unawaited Task, thread #1
#pragma warning restore 4014
}

private async Task Post(string action, string message, LogLevel logLevel)
{
    var jsonData = JsonConvert.SerializeObject(log); // #1
    var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); // #1

    var response = await httpClient.PostAsync(...), content);

    // Work here will be scheduled on any available Thread, after PostAsync completes #2
}
_

Re:例外処理

_try..catch_ブロックは非同期コードで機能します-await will 障害のあるTaskをチェックします そして例外を発生させます:

_ public async Task Post()
 {
     try
     {
         // ... other serialization code here ...
         await HttpPostAsync();
     }
     catch (Exception ex)
     {
         // Do you have a logger of last resort?
         Trace.WriteLine(ex.Message);
     }
 }
_

上記は例外を監視するための基準を満たしていますが、それでもグローバルレベルでUnobservedTaskExceptionハンドラーを登録することをお勧めします。

これは、例外の観察に失敗した場所を検出して特定するのに役立ちます。

_TaskScheduler.UnobservedTaskException += (sender, eventArgs) =>
{
    eventArgs.SetObserved();
    ((AggregateException)eventArgs.Exception).Handle(ex =>
    {
        // Arriving here is BAD - means we've forgotten an exception handler around await
        // Or haven't checked for `.IsFaulted` on `.ContinueWith`
        Trace.WriteLine($"Unobserved Exception {ex.Message}");
        return true;
    });
};
_

上記のハンドラーは、タスクがGCによって収集されたときにのみトリガーされることに注意してください。これは、例外の発生後しばらくかかる場合があります。

4
StuartLC