runtime: BackgroundService blocked the execution of whole host

Describe the bug

When I run the specific foreach cycle in BackgroundService-derived class, it blocks the whole host from starting. Even second hosted service doesn’t start. When I comment foreach cycle, everything works as expected.

To Reproduce

TargetFramework: netcoreapp2.1 Version: 2.1.12

Use following hosted service: NotificationRunner.cs And singleton: NotificationService.cs

Expected behavior

BackgroundService.ExecuteAsync should work in background without blocking even if it has blocking code. As you can see in NotificationService class, cancellation of enumerator is based on IApplicationLifetime.ApplicationStopping, but anyway it shouldn’t affect the host startup because BackgroundService is expected to run in background 😃

Screenshots

When foreach cycle exists image Then execution is blocked on this cycle image

But when foreach cycle is commented image Then execution continues as expected image (But why twice?)

Additional context

.NET Core SDK (reflecting any global.json):
 Version:   3.0.100-preview7-012821
 Commit:    6348f1068a

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.17763
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.0.100-preview7-012821\

Host (useful for support):
  Version: 3.0.0-preview7-27912-14
  Commit:  4da6ee6450

.NET Core SDKs installed:
  2.1.700 [C:\Program Files\dotnet\sdk]
  2.1.701 [C:\Program Files\dotnet\sdk]
  2.1.801 [C:\Program Files\dotnet\sdk]
  2.2.300 [C:\Program Files\dotnet\sdk]
  2.2.301 [C:\Program Files\dotnet\sdk]
  2.2.401 [C:\Program Files\dotnet\sdk]
  3.0.100-preview7-012821 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.All 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-preview7.19365.7 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0-preview7-27912-14 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 43
  • Comments: 54 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Fixed by adding await Task.Yield() at the top of method

Sorry I never did this change but enough people have hit it now I think we should just do it. https://github.com/aspnet/Extensions/tree/davidfowl/background-service

Just ran into this. Fixed with await Task.Yield() at top of BackgroundService.ExecuteAsync. I think this is especially confusing because we don’t call ExecuteAsync ourselves, the framework does. We don’t even instantiate the background service ourselves (we’re using services.AddHostedService<<MyBackgroundService>>()). So the fact that the instantiation of the service and the call to ExecuteAsync happen synchronously as part of the ASP.NET app startup is not obvious. In fact it’s downright strange imho.

I think it’s reasonable to dispatch ExecuteAsync to the thread-pool in StartAsync. If the user wants to perform initialization that must complete before letting the app continue, they can override StartAsync itself, right?

public class MyService: BackgroundService
{
	public override async Task StartAsync()
	{
		await InitializeAsync();
		await base.StartAsync();
	}

	public override async Task ExecuteAsync()
	{
		// Do background stuff...
	}
}

It’s called BackgroundService. It seems odd that you can block the foreground in the default case. If we’re not happy with overriding StartAsync we could add a new InitializeAsync that is called and awaited during StartAsync.

This would be a breaking change however, since existing services may depend on the initial synchronous work blocking the startup process.

Task.Run is a perfectly viable workaround. But I’d still like to see it baked into BackgroundService because it’s not currently a pit of success.

Most developers don’t think about async methods as starting synchronously. There’s a number of SO questions that are variants of “I started with a background service template and it works fine until I try to remove the Task.Delay” or “it stops working as soon as I try to [synchronously] read from a queue of work”. For those of us with a good understanding of async and how the host starts background services, the problem is obvious; but that’s not most developers.

This issue hasn’t been planned for 7.0 (see https://github.com/dotnet/runtime/issues/64015). Moving to Future.

I just ran into this and was about to leave feedback on the docs. Glad I checked here first. The Worker template works because it awaits a Task.Delay. Change that to a Thread.Sleep, remove async keyword, and return a CompletedTask from ExecuteAsync and it can be easily reproduced. Execution is not returned to the caller so StartAsync never finishes and the host never finishes initialization so cancellationtoken does not work and any other HostedServices registered would never start.

I have also encountered this issue and solved it in the same way as @vova-lantsov-dev did. However, my main concern is that this behavior is not straightforward, would be nice if this was more predictable or documented somewhere

For what it’s worth:

The argument that you can just override StartAsync to accomplish the same thing is basically true but there is a pretty notable drawback: Now, state that you used to be able to initialize and use locally in ExecuteAsync has to be promoted to mutable class state. This gets particularly ugly when NRTs are enabled.

This:

class MyService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        var state = ...;

        await Task.Yield();

        while (true)
        {
            Work(state);

            await Task.Delay(..., ct);
        }
    }
}

Turns into this:

class MyService : BackgroundService
{
    T? _state;

    public override Task StartAsync(CancellationToken ct)
    {
        _state = ...;

        return base.StartAsync(ct);
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (true)
        {
            Work(_state!);

            await Task.Delay(..., ct);
        }
    }
}

This seems like a significant regression in code clarity to me.

There’s also an argument to be made that, if the current behavior of ExecuteAsync is misleading, then so too is the StartAsync override in the example above.

The name BackgroundService is definitely misleading given the current behavior. But I also think that the current behavior is actually useful - I rely on it in two of my projects for sane synchronous initialization ordering. So, I hope that if this change does go through, an alternative class will be provided that retains the current behavior.

The IHostedLifecycleService and ServicesStartConcurrently changes in .NET 8.0 do not address this issue. The host (as currently written) still starts the services by calling StartAsync directly. So a synchronous implementation of StartAsync will still block host startup.

While implementing BackgroundService it worked without any Task.Yield(), but today it doesn’t. Does anyone know how that is possible?

Look at the implementation of BackgroundService.

StartAsync is a synchronous (!!) method which returns either the Task taken as result from ExecuteAsync, if said task IsCompleted; and otherwise returns a pre-completed Task.CompletedTask. It will take responsbility to stop the execute task in StopAsync by triggering the cancellation token, regardless.

Because StartAsync is synchronous, the code will block until the actual ExecuteAsync implementation, if it is an async method, does its first await. That first await is where the asynchronous state machine compiler magic effectively kicks in and the async method returns its Task in a yet-to-be-completed state.

I’m not a fan as it’s wasteful, but if that many people run into it it might be worth doing. I’d honestly rather teach people that these things aren’t running on dedicated threads so blocking them isn’t the way to go.

Async method executes synchronously till first await is reached (it means BackgroundService.ExecuteAsync won’t return till await is called). So this problem is not related to ASP.NET Core, but to async compilation.

Wow what a wierd behaviour from a “background service”

.NET 8 has been released, and unfortunately, Microsoft has turned a blind eye to this issue. For those who have encountered it, it would be wise not to use BackgroundService. Instead, you can create your implementation based on IHostedService or the new IHostedLifecycleService.

The situation is amusing - to start a synchronous service, you need to await something, for example, Task.Yield, resulting in the generation of a state machine and a loss of at least 0.05% performance at this stage. Alternatively, you can use await Task.Run and lose at least 0.10% simply because of the presence of a state machine to start the service elegantly with just one addition to the DI container.

I’ve run into this too. @eerhardt could we make this happen in 8.0? this could be a relatively simple fix compared to the time spent scratching their heads 😃

So, I tried the above code, and apparently the issue only happens on Mac, on both vscode and vs for Mac.

I’m having the same problem using an IAsyncEnumerable in the background thread. The operation shouldn’t be blocking (it’s an async Enumerable after all) but @pawepaw seems to be right about this. For now, the Task.Yield workaround will do it for me.

Facing same problem that background tasks is blocking main thread with IAsyncEnumerable. That’s probably due to IAsyncEnumerable implementation https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8#under-the-hood-of-async-iterators

@HelloKitty IHostedService.StartAsync always blocks the execution of the whole host. This is expected behaviour. Try to use BackgroundService.ExecuteAsync for non-blocking delayed tasks.

Reopening this so we can look at it. I’m glad that you solved your problem but we should try to solve this so others don’t hit it.