dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.03k stars 4.68k forks source link

StopAsync of BackgroundServices is called sequentially #58715

Open fgheysels opened 3 years ago

fgheysels commented 3 years ago

I've noticed that, if you have multiple BackgroundServices, the StopAsync method of those services is not called in parallel when the host is stopping. They're rather called in a blocking fashion.

Suppose:

 static async Task Main(string[] args)
{
    var host = Host.CreateDefaultBuilder(args)
                   .ConfigureServices(services =>
                   {
                       services.AddHostedService<Worker1>();
                       services.AddHostedService<Worker2>();
                   })
                   .UseConsoleLifetime()
                   .Build();

    await host.RunAsync(CancellationTokenSource.Token);
}

And we have these backgroundservices:

public class Worker1 : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (stoppingToken.IsCancellationRequested == false)
        {
            Console.WriteLine("working");
            await Task.Delay(TimeSpan.FromMilliseconds(2000), stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Stop called for worker 1");
        await base.StopAsync(cancellationToken);
    }
}

public class Worker2 : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (stoppingToken.IsCancellationRequested == false)
        {
            Console.WriteLine("working 2");
            await Task.Delay(TimeSpan.FromMilliseconds(2000), stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("stop called for 2");
        await Task.Delay(TimeSpan.FromSeconds(10));
        await base.StopAsync(cancellationToken);
        Console.WriteLine("stop 2 finished");
    }
}

When running the application, and then pressing ctrl+c, we see this output:

image

This means that the StopAsync method for the last registered backgroundservice is called first, and the runtime is waiting for this StopAsync method to finish before StopAsync for the other worker is called.

Is there a reason why the StopAsync methods are not called in parallel, and all the returned tasks are awaited ? Something like this ? (pseudo-code off course)

var services = this.HostedServices;

var stopTasks = new List<Task>();

foreach( var service in services )
{
    stopTasks.Add(service.StopAsync());
}

await Task.WhenAll(stopTasks);
dotnet-issue-labeler[bot] commented 3 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

fgheysels commented 3 years ago

Best label would be area-Extensions-Hosting imho.

ghost commented 3 years ago

Tagging subscribers to this area: @eerhardt, @maryamariyan See info in area-owners.md if you want to be subscribed.

Issue Details
I've noticed that, if you have multiple `BackgroundServices`, the `StopAsync` method of those services is not called in parallel when the host is stopping. They're rather called in a blocking fashion. Suppose: ```csharp static async Task Main(string[] args) { var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddHostedService(); services.AddHostedService(); }) .UseConsoleLifetime() .Build(); await host.RunAsync(CancellationTokenSource.Token); } ``` And we have these backgroundservices: ```csharp public class Worker1 : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (stoppingToken.IsCancellationRequested == false) { Console.WriteLine("working"); await Task.Delay(TimeSpan.FromMilliseconds(2000), stoppingToken); } } public override async Task StopAsync(CancellationToken cancellationToken) { Console.WriteLine("Stop called for worker 1"); await base.StopAsync(cancellationToken); } } public class Worker2 : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (stoppingToken.IsCancellationRequested == false) { Console.WriteLine("working 2"); await Task.Delay(TimeSpan.FromMilliseconds(2000), stoppingToken); } } public override async Task StopAsync(CancellationToken cancellationToken) { Console.WriteLine("stop called for 2"); await Task.Delay(TimeSpan.FromSeconds(10)); await base.StopAsync(cancellationToken); Console.WriteLine("stop 2 finished"); } } ``` When running the application, and then pressing ctrl+c, we see this output: ![image](https://user-images.githubusercontent.com/3605786/132183334-01629b00-b0ae-46f9-a4f3-deff86a2608e.png) This means that the `StopAsync` method for the last registered backgroundservice is called first, and the runtime is waiting for this `StopAsync` method to finish before `StopAsync` for the other worker is called. Is there a reason why the `StopAsync` methods are not called in parallel, and all the returned tasks are awaited ? Something like this ? (pseudo-code off course) ``` var services = this.HostedServices; var stopTasks = new List(); foreach( var service in services ) { stopTasks.Add(service.StopAsync()); } await Task.WhenAll(stopTasks); ```
Author: fgheysels
Assignees: -
Labels: `untriaged`, `area-Extensions-Hosting`
Milestone: -
eerhardt commented 3 years ago

It is the same for StartAsync, they are started sequentially and not in parallel.

https://github.com/dotnet/runtime/blob/df6e28ee6312184a5ee9c64f6460bd86ea5886e1/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs#L63-L66

https://github.com/dotnet/runtime/blob/df6e28ee6312184a5ee9c64f6460bd86ea5886e1/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs#L119-L123

In your above code example, if you call base.StopAsync right away, your ExecuteAsync method will stop running right away:

public override async Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("stop called for 2");
        await base.StopAsync(cancellationToken);
        await Task.Delay(TimeSpan.FromSeconds(10));
        Console.WriteLine("stop 2 finished");
    }
KalleOlaviNiemitalo commented 6 months ago

Concurrent StopAsync was implemented in https://github.com/dotnet/runtime/pull/84048. I suggest closing this issue as a duplicate of https://github.com/dotnet/runtime/issues/68036.