这里的 BackgroundService 是指:
Microsoft.Extensions.Hosting.BackgroundService
1. 问题复现
继承该BackgroundService,实现自己的MyService :
public class MyService : BackgroundService { private CancellationTokenSource _CancelSource; public async Task StartAsync() { _CancelSource = new CancellationTokenSource(); await base.StartAsync(_CancelSource.Token); Console.WriteLine("Start"); } public async Task CancelAsync() { await base.StopAsync(_CancelSource.Token); Console.WriteLine("Stop"); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await Task.Delay(1000, stoppingToken).ContinueWith(tsk => { Console.WriteLine(string.Format("{0} working...", DateTime.Now.ToString("mm:ss"))); }); } } }
实例化后可以启动运行,然后停止。但是再次调用该实例的 StartAsync()就启动不了了。
当然,从 BackgroundService 实现的接口(IHostedService)来看,可能这个类本身就没打算让你手动控制启停。
2. 原因
一句话概括就是
作为函数输入参数的 CancellationToken 并没有用到
这也就是难怪网上的教程都是直接使用 CancellationToken.None 作为输入参数的原因。
下面是详细分析
直接贴下 BackgroundService 的 源码:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Threading; using System.Threading.Tasks; namespace Microsoft.Extensions.Hosting { /// <summary> /// Base class for implementing a long running <see cref="IHostedService"/>. /// </summary> public abstract class BackgroundService : IHostedService, IDisposable { private Task _executingTask; private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource(); /// <summary> /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task that represents /// the lifetime of the long running operation(s) being performed. /// </summary> /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param> /// <returns>A <see cref="Task"/> that represents the long running operations.</returns> protected abstract Task ExecuteAsync(CancellationToken stoppingToken); /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start process has been aborted.</param> public virtual Task StartAsync(CancellationToken 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; } /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param> public virtual async Task StopAsync(CancellationToken cancellationToken) { // Stop called without start if (_executingTask == null) { return; } try { // Signal cancellation to the executing method _stoppingCts.Cancel(); } finally { // Wait until the task completes or the stop token triggers await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)); } } public virtual void Dispose() { _stoppingCts.Cancel(); } } }
可以看到上面 StartAsync 函数调用 ExecuteAsync 时给它赋的参数直接是一个内部的只读变量,你在外部调用 StartAsync 给它输入的参数根本就没有用到。
结果就是,调用 StopAsync 之后,_stoppingCts 触发了Cancel请求,那么 _stoppingCts.IsCancellationRequested 就变成了 true,因为是只读的,所以再次调用StartAsync 来启动,进入 ExecuteAsync 之后 while判断直接就是false跳出了。
3. 解决办法
方法一:跳过StartAsync、StopAsync ,直接调用 ExecuteAsync ;
方法二:仿照官方的 BackgroundService,实现 IHostedService 接口,自己写一个 BackgroundService
方法三:使用 BackgroundWorker