如何在 IHostedService 中启动和停止任务

问题描述 投票:0回答:2

我创建了一个 ASP.NET Core Web API,但我不知道如何正确实现 IHostedService。我有几个需要作为后台进程运行的“worker”类,因此我使用 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();
    }
}

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方法中正确取消所有正在运行的任务并等待它们完成?

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

c# asp.net-core asp.net-core-webapi task ihostedservice
2个回答
2
投票

您的代码有几个问题。

  1. 您没有将取消令牌传递给
    Task.Delay()
    ,并且很有可能您的工作人员将在这种方法中停留相当长一段时间。 (此外,我知道您可能会这样做,以便您可以测试取消,但按照编码,它会干扰您的测试)。
  2. cts.Token
    给出的标记与
    cancellationToken
    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,然后取消
run 1 result run 2 result

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


1
投票

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

cts.Cancel();
Task.WaitAll(workersToRun.ToArray(), TimeSpan.FromSeconds(30));
© www.soinside.com 2019 - 2024. All rights reserved.