在 IHostedService 中启动和停止任务

Start and stop tasks in IHostedService

我已经创建了一个 ASP.NET 核心 API 但我不知道如何正确地实现 IHostedService。 我有几个“工人”类需要运行作为后台进程,所以我使用 IHostedService 异步启动所有任务。

Startup.cs:

services.AddHostedService<BackgroundService>();

BackgroundService.cs:

public class BackgroundService: IHostedService
{
    private CancellationTokenSource cts = new CancellationTokenSource();
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        return RunTasks (cts.Token);
    }

    private List<IWorker> workersToRun = new List<IWorker>();
    private Task RunTasks(CancellationToken cancellationToken)
    {
        try
        {
            Worker1 w1 = new Worker1(); //Implements IWorker
            workersToRun.Add(Task.Run(() => w1.DoWork(cancellationToken)));
            
            Worker1 w2 = new Worker2(); //Implements IWorker
            workersToRun.Add(Task.Run(() => w2.DoWork(cancellationToken)));

            Task.WhenAll(workersToRun.ToArray());
            
            return Task.CompletedTask;
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
            throw;
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        try
        {
            cts.Cancel();
        }
        finally
        {
            //Wait for all started workers/tasks to complete ??????
        }

        return Task.CompletedTask;
    }

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

Worker.cs

public interface IWorker
{
    Task DoWork(CancelationToken token)
}

public class Worker1 : IWorker
{
    public Task DoWork(CancelationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            return Task.Delay(1000);
            //Do some random stuff in the background
        }
        
        //cleanup
    }
}

public class Worker2 : IWorker
{
    public async Task DoWork(CancelationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            await Task.Delay(1000); 
            //Do some random async stuff in the background
        }
        
        //cleanup
    }
}

worker 类 似乎可以工作(根据日志),但 cancelationToken 不工作,因此清理代码永远不会执行(似乎)。 如何在 StopAsync 方法中正确取消所有 运行ning 任务并等待它们完成?

(上面的所有代码都是简化的,它实际上包含了 di 和错误处理,但那是不相关的)

您已请求取消,但您没有等待足够长的时间让任务对其做出反应。我会指定一个超时时间,这样拒绝取消的工作人员就不会停止进程。

cts.Cancel();
Task.WaitAll(workersToRun.ToArray(), TimeSpan.FromSeconds(30));

您的代码有几个问题。

  1. 您没有将取消令牌传递给 Task.Delay(),您的工作人员很可能会在这种方法中停留很长一段时间。 (另外,我知道您可能会延迟,以便您可以测试取消,但编码会干扰您的测试)。
  2. cts.Token 给出的令牌与 BackgroundServicecancellationToken 参数不同(很有可能)。
  3. 您正在 StartAsync 中执行 Task.WhenAll(),当您对其应用 await 时,它将阻止启动,直到所有任务实际完成。我不认为那是你想要的。
  4. 取消是合作的,因此您需要在延迟后立即检查取消。当您“真正”删除延迟时,请务必在将要实现的任何内容中添加取消检查 //Do some random async stuff in the background,包括将令牌传递到您的异步堆栈中。
  5. 最后,我建议async/await“一路走下去”。

下面是一些消除上述问题的代码。

public class BackgroundService : IHostedService
{
    private readonly CancellationTokenSource cts;

    public BackgroundService(CancellationTokenSource cts) => this.cts = cts;

    public async Task StartAsync(CancellationToken cancellationToken) => await RunTasks (cancellationToken);

    private List<Task> workersToRun = new List<Task>();

    private async Task RunTasks(CancellationToken cancellationToken)
    {
        try
        {
            // tasks are started immediately below
            var w1 = new Worker1();
            workersToRun.Add(Task.Run(async () => await w1.DoWork(cancellationToken)));
        
            var w2 = new Worker2();
            workersToRun.Add(Task.Run(async () => await w2.DoWork(cancellationToken)));

            // no Task.WhenAll() here. If you do that, RunTasks() will be blocked until they complete!
            await Task.CompletedTask;
        }
        catch (Exception ex)
        {
            Program.WriteLog(ex.Message);
        }
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // skip cancellation if we don't need it
        if (workersToRun.All(x => x.IsCompleted))
            return;

        try
        {
            Program.WriteLog("Call Cancel()");
            cts.Cancel();
        }
        finally
        {
            // wait for all started workers/tasks to complete
            Program.WriteLog("WhenAll()");
            await Task.WhenAll(workersToRun);
        }
    }

    ...
}

和其中一名工人,

public class Worker1 : IWorker
{
    public async Task DoWork(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(1000, token);
            }
            catch (TaskCanceledException)
            {
                Program.WriteLog("Worker1 cancelled in Delay()");
                break;                    
            }
            
            if (!token.IsCancellationRequested)
                Program.WriteLog("Doing work in Worker1");
        }
    
        Program.WriteLog("Worker1 completed; clean up");
    }
}

最后 driver:

class Program
{
    public static List<string> log = new List<string>();

    public static void WriteLog(string s)
    {
        lock (log) log.Add(s);
    }

    static async Task Main(string[] args)
    {
        WriteLog("Start");
        var cts = new CancellationTokenSource();
        var service = new BackgroundService(cts);
        WriteLog("Call StartAsync()");
        await service.StartAsync(cts.Token);
        WriteLog("Wait 500ms");
        await Task.Delay(1500);
        WriteLog("Call StopAsync()");
        await service.StopAsync(cts.Token);
        WriteLog("Done");
        log.ForEach(Console.WriteLine);
        Console.ReadLine();
    }
}

这是您在开始第一个 运行 后 500 毫秒取消,然后在第二个 运行 取消 1500 毫秒后的结果(通过使用 [=14 提供的令牌调用 StopAsync =]). 您会注意到第一个 运行 没有完成任何工作,第二个完成了“一个单元”的工作。

Run 1 Run 2
500ms, then cancel 1500ms, then cancel

这是有道理的;在第一个中,您在真正的工作开始之前取消了,在第二个中,您在完成 1 个工作单元后但在第二个单元之前取消了。