.NET Core中运行时间短的任务

Mak*_*kla 9 c# .net-core asp.net-core

我刚刚发现了IHostedService.NET Core 2.1 BackgroundService类.我觉得这个想法太棒了.文档.

我发现的所有示例都用于长时间运行的任务(直到应用程序死亡).但我需要它很短的时间.这样做的正确方法是什么?

例如:
我想在应用程序启动后执行一些查询(大约需要10秒钟).并且只有在开发模式下.我不想延迟应用程序启动,所以IHostedService似乎很好的方法.我不能使用Task.Factory.StartNew,因为我需要依赖注入.

目前我这样做:

public class UpdateTranslatesBackgroundService: BackgroundService
{
    private readonly MyService _service;

    public UpdateTranslatesBackgroundService(MyService service)
    {
        //MService injects DbContext, IConfiguration, IMemoryCache, ...
        this._service = service;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await ...
    }
}
Run Code Online (Sandbox Code Playgroud)

启动:

public static IServiceProvider Build(IServiceCollection services, ...)
{
    //.....
    if (hostingEnvironment.IsDevelopment())
        services.AddSingleton<IHostedService, UpdateTranslatesBackgroundService>();
    //.....
}
Run Code Online (Sandbox Code Playgroud)

但这似乎有点矫枉过正.是吗?注册singleton(表示类在应用程序存在时存在).我不需要这个.只需创建类,运行方法,配置类.全部在后台任务.

Fil*_*das 6

好吧,我认为这里有不止一个问题。首先让我指出一些你可能知道的 async != multithreaded。所以 BackgroundService 不会让你的应用程序“多线程”,它可以毫无问题地在单线程内运行。如果你在那个线程上做阻塞操作,它仍然会阻塞启动。让我们说在类中,您以非真正的异步方式实现所有 sql 查询,类似于

public class StopStartupService : BackgroundService
    {
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            System.Threading.Thread.Sleep(1000);
            return Task.CompletedTask;
        }
    }
Run Code Online (Sandbox Code Playgroud)

这仍然会阻止启动。

所以还有一个问题。

您应该如何运行后台作业?

为此,在简单的情况下Task.Run(如果您不确定如何配置它,请尽量避免使用 Task.Factory.StartNew)应该可以完成这项工作,但这并不是说这是最好的或好的方法。有很多开源库可以为你做这件事,看看它们提供的东西可能会很好。有很多问题您可能没有意识到,如果您只是使用Task.Run 这些问题,可能会产生令人沮丧的错误。我可以看到的第二个问题是。

我应该在 C# 中开火并忘记吗?

对我来说,这是一个明确的不(但 XAML 人可能不同意)。不管你做什么,你都需要跟踪你正在做的事情是什么时候完成的。在您的情况下,如果有人在查询完成之前停止应用程序,您可能希望在数据库中进行回滚。但不仅如此,您还想知道何时可以开始使用查询提供的数据。因此BackgroundService可以帮助您简化执行但很难跟踪完成情况。

你应该使用单例吗?

正如您已经提到的,使用单例可能是一件危险的事情,特别是如果您没有正确清理事物,但更重要的是,您正在使用的服务的上下文在对象的生命周期内将是相同的。因此,这一切都取决于您对服务的实施是否会出现问题。

我做这样的事情来做你想做的事。

   public interface IStartupJob
    {
        Task ExecuteAsync(CancellationToken stoppingToken);
    }

    public class DBJob : IStartupJob
    {
        public Task ExecuteAsync(CancellationToken stoppingToken)
        {
            return Task.Run(() => System.Threading.Thread.Sleep(10000));
        }
    }

    public class StartupJobService<TJob> : IHostedService, IDisposable where TJob: class,IStartupJob
    {
        //This ensures a single start of the task this is important on a singletone
        private readonly Lazy<Task> _executingTask;

        private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

        public StartupJobService(Func<TJob> factory)
        {
            //In order for the transient item to be in memory as long as it is needed not to be in memory for the lifetime of the singleton I use a simple factory
            _executingTask = new Lazy<Task>(() => factory().ExecuteAsync(_stoppingCts.Token));
        }

        //You can use this to tell if the job is done
        public virtual Task Done => _executingTask.IsValueCreated ? _executingTask.Value : throw new Exception("BackgroundService not started");

        public virtual Task StartAsync(CancellationToken cancellationToken)
        {

            if (_executingTask.Value.IsCompleted)
            {
                return _executingTask.Value;
            }

            return Task.CompletedTask;
        }

        public virtual async Task StopAsync(CancellationToken cancellationToken)
        {
            if (_executingTask == null)
            {
                return;
            }

            try
            {
                _stoppingCts.Cancel();
            }
            finally
            {
                await Task.WhenAny(_executingTask.Value, Task.Delay(Timeout.Infinite,
                                                              cancellationToken));
            }

        }

        public virtual void Dispose()
        {
            _stoppingCts.Cancel();
        }

        public static void AddService(IServiceCollection services)
        {
            //Helper to register the job
            services.AddTransient<TJob, TJob>();

            services.AddSingleton<Func<TJob>>(cont => 
            {
                return () => cont.GetService<TJob>();
            });

            services.AddSingleton<IHostedService, StartupJobService<TJob>>();
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • 这主要是错误的。调用 `await` *将*产生一个新线程,因为 .net core 中没有同步上下文。参见 https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html。后台服务*不会*阻止启动。Async 确实意味着它是一个多线程应用程序。 (2认同)

Dou*_*oug 5

没有必要为这个工作做任何魔法。

简单地:

  • 注册您需要运行的服务 ConfigureServices
  • 解析您需要的实例Configure并运行它。
  • 为避免阻塞,请使用Task.Run.

必须注册实例,否则依赖注入将不起作用。这是不可避免的;如果您需要DI,那么您必须这样做。

除此之外,做你问的事情很简单,就像这样:

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }

  public IConfiguration Configuration { get; }

  // This method gets called by the runtime. Use this method to add services to the container.
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddTransient<MyTasks>(); // <--- This
  }

  // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  {
    if (env.IsDevelopment())
    {
      app.UseDeveloperExceptionPage();

      // Blocking
      app.ApplicationServices.GetRequiredService<MyTasks>().Execute();

      // Non-blocking
      Task.Run(() => { app.ApplicationServices.GetRequiredService<MyTasks>().Execute(); });
    }
    else
    {
      app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
  }
}

public class MyTasks
{
  private readonly ILogger _logger;

  public MyTasks(ILogger<MyTasks> logger)
  {
    _logger = logger;
  }

  public void Execute()
  {
    _logger.LogInformation("Hello World");
  }
}
Run Code Online (Sandbox Code Playgroud)

BackgroundService为长时间运行的进程而存在;如果是一次,请不要使用它。