在一个 ASP.NET
程序中我们常常需要进行比较耗时间的任务, 通常我们不会在一个请求中等待这些任务完成后再返回响应, 比如发邮件, 短信等. 这时候就需要我们将任务放到后台运行, 然后直接返回响应, 告诉用户任务已经再进行了. 本文将教会大家如何在 .NET
中运行后台任务, 示例代码请点击链接
创建 Worker
程序
.NET
自带了一个后台任务模板, 我们执行 dotnet new worker -o BackgroundTask
就可以创建一个通用的 console
后台任务程序. 该模板中包含一个 Worker
类型, 继承了 BackgroundService
, 然后在 HostBuilder
中通过 AddHostedService
注册. 最后调用 host.RunAsync()
执行该后台任务. 该示例程序每隔一秒钟打印当前时间.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| namespace BackgroundTask;
public class Worker : BackgroundService { private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger) { _logger = logger; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken); } } }
|
1 2 3 4 5 6 7 8 9 10
| using BackgroundTask;
IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddHostedService<Worker>(); }) .Build();
await host.RunAsync();
|
AddHostedService
接受一个 IHostedService
的泛型参数, 其有两个方法 StartAsync
和 StopAsync
. 一个 IHostedService
跟一个单例 Service
相似, 只是启动时会调用 StartAsync
方法, 结束时调用 StopAsync
方法.
BackgroundService
实际上是一个 IHostedService
长时间运行的实现.
现在我们添加一个 SimpleWorker
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| namespace BackgroundTask;
public class SimpleWorker : IHostedService { private ILogger<SimpleWorker> _logger;
public SimpleWorker(ILogger<SimpleWorker> logger) { _logger = logger; }
public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Simple Worker Start"); return Task.CompletedTask; }
public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Simple Worker Stop"); return Task.CompletedTask; } }
|
我们还需要在 Program.cs
中添加注册.
1
| services.AddHostedService<SimpleWorker>();
|
现在在启动和结束时会分别打印信息.
使用 Timer
进行定时任务
默认的 Worker
实现定时每秒钟打印执行的效果, 我们也可以使用 Timer
可以获得同样的效果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| namespace BackgroundTask;
public class TimerWorker : IHostedService { private int _count = 0; private ILogger<TimerWorker> _logger; private Timer? _timer;
public TimerWorker(ILogger<TimerWorker> logger) { _logger = logger; }
public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Timer Worker Start"); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask; }
public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Timer Worker Stop");
return Task.CompletedTask; }
private void DoWork(object? state) { var count = Interlocked.Increment(ref _count);
_logger.LogInformation($"count = {count}, time: {DateTimeOffset.Now}"); } }
|
同时别忘了注册该 Service
.
使用 Channel
创建任务队列
更常见的使用后台任务的方式是使用队列, .NET
为我们提供了 System.Threading.Channel
类. 这里我们需要两个 Worker
: 一个作为生产者, 一个作为消费者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| namespace BackgroundTask;
using System.Threading.Channels;
public class ProducerWorker : BackgroundService { private int _count = 0; private Channel<int> _queue;
public ProducerWorker(Channel<int> queue) { _queue = queue; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var count = Interlocked.Increment(ref _count); await _queue.Writer.WriteAsync(count, stoppingToken); await Task.Delay(1000, stoppingToken); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| namespace BackgroundTask;
using System.Threading; using System.Threading.Channels; using System.Threading.Tasks;
public class ConsumerWorker : BackgroundService { private Channel<int> _queue; private ILogger<ConsumerWorker> _logger;
public ConsumerWorker(Channel<int> queue, ILogger<ConsumerWorker> logger) { _queue = queue; _logger = logger; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var count = await _queue.Reader.ReadAsync(stoppingToken);
_logger.LogInformation($"do work for: {count}"); } } }
|
同时我们需要通过依赖注入注入一个 Channel<int>
的单例:
1 2 3 4
| var queue = Channel.CreateBounded<int>(10); services.AddSingleton(queue); services.AddHostedService<ProducerWorker>(); services.AddHostedService<ConsumerWorker>();
|