前言
我们常用 BackgroundService
创建执行后台任务或执行长时间运行任务的进程。
但是在实际使用中,我们发现,如果 BackgroundService
在执行中发生异常,有时会抛出异常,中断程序;有时即使出错,也没有任何日志输出。
这显然是不对的,应该始终抛出异常。.
那么,这到底是怎么回事呢?
问题重现
通过检查我们的实现代码,我们发现,这和 await
出现的位置有关,如果在 await
执行前产生异常,则会抛出,否则静默。
例如下列代码:
//抛出异常
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
throw new Exception();
await Task.Delay(5000);
}
//无异常
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(5000);
throw new Exception();
}
原因分析
查看 BackgroundService
的源代码[1],它实际是在启动时执行的 ExecuteAsync
方法:
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Create linked token to allow cancelling executing task from provided token
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Store the task we're executing
_executingTask = ExecuteAsync(_stoppingCts.Token);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted)
{
return _executingTask;
}
// Otherwise it's running
return Task.CompletedTask;
}
问题的关键在这里:
if (_executingTask.IsCompleted)
{
return _executingTask;
}
return Task.CompletedTask;
-
如果
ExecuteAsync
在await
前报错,则_executingTask.IsCompleted == true
,返回了_executingTask
,因此可以捕获到到异常。 -
如果
ExecuteAsync
在await
前未报错,由于未执行await ExecuteAsync(_stoppingCts.Token)
,程序会继续往下执行,则_executingTask.IsCompleted == false
,BackgroundService
将返回一个默认的CompletedTask
, 这样我们永远无法知道_executingTask
是否有异常。(相关问题请参看我以前的文章《如何保证执行异步方法时不会遗漏 await 关键字》)
解决问题
虽然我们可以在 ExecuteAsync
方法实现内部使用 try-catch
来规避这类问题。
但是,既然知道了原因,我们能否在不修改现有 ExecuteAsync
方法实现的情况下解决?
我们的想法是,让 BackgroundService
始终检查 _executingTask
是否有异常。
虽然原始的 BackgroundService
做不到,但是不妨碍我们装饰它一下。
装饰者模式
Decorator Pattern(装饰者模式),是指在不必改变原类文件和使用继承的情况下,动态地给一个对象添加一些额外的职责。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
在 .NET 框架中就大量使用了装饰者模式,比如 CryptoStream
就是 Stream
的装饰类,通过在写入底层之前加密数据,并在读取数据之前解密来扩展行为。
实现
我们创建 BackgroundService
的装饰类 CatchExceptionBackgroundService<T>
:
public class CatchExceptionBackgroundService<T> : BackgroundService where T : BackgroundService
{
private readonly T _backgroundService;
private Task _executingTask;
public CatchExceptionBackgroundService(T backgroundService)
{
this._backgroundService = backgroundService;
}
public override Task StartAsync(CancellationToken cancellationToken)
{
var task = _backgroundService.StartAsync(cancellationToken);
_executingTask = typeof(BackgroundService).GetField("_executingTask", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(_backgroundService) as Task;
Timer timer = new Timer(CheckStatus,null,1000, 1000);
return task;
}
public override Task StopAsync(CancellationToken cancellationToken)
{
return _backgroundService.StopAsync(cancellationToken);
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.CompletedTask;
}
}
-
使用构造函数注入实际的 BackgroundService
-
StartAsync
和StopAsync
调用实际的BackgroundService
实例对应方法 -
通过反射获取实际的 BackgroundService
实例中的_executingTask
-
定时检查 _executingTask
是否抛出异常
用于检查代的码如下:
public void CheckStatus(Object stateInfo)
{
if (_executingTask != null && _executingTask.IsCompleted)
{
if (_executingTask.Exception is AggregateException exception)
{
_executingTask = null;
throw exception.InnerException;
}
_executingTask = null;
}
}
使用
只需修改 Startup.cs
, 将原来的注册方法改为使用装饰类:
public void ConfigureServices(IServiceCollection services)
{
//原来的注册方法
//services.AddHostedService<DemoBackgroundService>();
services.AddSingleton<DemoBackgroundService>();
services.AddHostedService<CatchExceptionBackgroundService<DemoBackgroundService>>();
}
DemoBackgroundService
的实现代码如下:
public class DemoBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("DemoBackgroundService.ExecuteAsync");
await Task.Delay(5000);
throw new Exception("DemoBackgroundService throw Exception");
}
}
可以看到,工作正常:
结论
今天,我们使用装饰者模式,实现始终捕获 BackgroundService
产生的异常。