【问题标题】:Safe way to implement a "Fire and Forget" method on ASP.NET Core在 ASP.NET Core 上实现“一劳永逸”方法的安全方法
【发布时间】:2018-06-08 08:22:40
【问题描述】:

我正在尝试实现一个简单的日志库,它将用于多个项目。库的工作是将 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
    }
}

这就是我在我的 web api 的 Startup 类的 ConfigureServices 方法中注册记录器的方式:

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

     services.AddSingleton<ILogger, Logger>();

     // .....
}

此代码在我的 web api 中的一个方法中:

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

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

   // Here also some business logic

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

【问题讨论】:

标签: c# asynchronous asp.net-core async-await task


【解决方案1】:

Re : 对异步方法的未等待调用 vs Task.Run()

由于Post 中只有少量 CPU 绑定工作(即创建 json 有效负载),因此另一个 Task.Run 没有任何好处 - 在 Threadpool 上安排新任务的开销将超过 IMO 的任何好处。即

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

是两种方法中较好的一种。您可能希望禁止在未等待的任务中关联的编译器警告,并为下一个开发人员遇到代码留下评论。

但根据 Stephen Cleary 的明确回答,请在 ASP.Net is almost never a good idea 中解雇并忘记。最好是卸载工作,例如通过队列,到 Windows 服务、Azure Web Job 等。

还有额外的危险 - 如果未等待的任务抛出,你会想observe the exception

另外,请注意,在 Post 之后完成的任何工作(例如,如果您使用 response)这仍然是需要在线程池上安排的延续任务 - 如果您触发大量 @ 987654331@ 方法,当它们完成时你会遇到很多线程争用。

Re : 另外,如果我不将 await 与 Task.Run() 一起使用,我会阻塞线程吗?

awaitdoesn't require a threadawait 是要求编译器异步重写代码的语法糖。 Task.Run() 将在 ThreadPool 上安排第二个任务,该任务在到达 PostAsync 方法之前只会做少量工作,因此建议不要使用它。

InfoPost 的未等待调用的调用者线程使用/阻塞量取决于在返回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
}

回复:异常处理

try..catch 块使用异步代码 - awaitcheck for a faulted 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;
    });
};

注意,上面的handler只在Task被GC回收时触发,可能是在异常发生后的一段时间。

【讨论】:

  • 问题不在于对异常的观察。问题是如果发生异常,它很可能最终导致您的应用程序崩溃,除非有一个应用程序范围的捕获器。此外,当请求结束时,瞬态/范围服务将被释放并且不能在那里使用。有后台服务的队列绝对是正确的做法
  • There was a change UnobservedTaskExceptions 不应再使进程崩溃,但是是的,F+F 不是前进的方向。
  • @StuartLC 感谢您的回复。如果我不关心 Task 抛出的异常怎么办?此外,我在问题中添加了更多代码以使其更清晰。
  • 链接的答案表明未观察到的 Task 的行为。 async void 不同,永远不应在 UI 事件回调(WPF、Windows 窗体)之外使用。 @Nomad:这不会改变您的服务主题可能在您的操作完成之前被处置的任何内容
  • @Tseng 你的意思是在.NET Core UnobservedTaskException 中仍然会导致进程崩溃吗?所以如果我将我的记录器添加为单例服务,并且库中会有 UnobservedTaskException,我的应用程序(web api)会崩溃吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-16
  • 2016-07-20
  • 2017-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多