1

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

启动.cs:

services.AddHostedService<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();
    }
}

工人.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
    }
}

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

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

4

2 回答 2

1

您的代码有几个问题。

  1. 您没有将取消令牌传递给Task.Delay(),而且很可能您的工作人员将在此方法中停留很长一段时间。(除此之外,我知道您可能会延迟,以便您可以测试取消,但按照编码它会干扰您的测试)。
  2. 给出的令牌与(最有可能)cts.TokencancellationToken参数不同。BackgroundService
  3. 你正在做Task.WhenAll()StartAsync,当你申请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");
    }
}

最后是司机:

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 毫秒后取消的结果(通过StopAsync使用 提供的令牌进行调用cts.Token)。您会注意到在第一次运行中没有完成任何工作,而在第二次运行中您完成了“一个单元”的工作。

运行 1 运行 2
500ms,然后取消 1500ms,然后取消
运行 1 个结果 运行 2 结果

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

于 2021-06-25T19:45:21.630 回答
1

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

cts.Cancel();
Task.WaitAll(workersToRun.ToArray(), TimeSpan.FromSeconds(30));
于 2021-06-25T19:39:53.100 回答