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
Then execution is blocked on this cycle
But when foreach
cycle is commented
Then execution continues as expected
(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)
Links to this issue
Commits related to this issue
- Call Task.Yield() in TokenCleanupService so they don't block the app execution https://github.com/dotnet/runtime/issues/36063 WaitHandle.WaitOne replaced with Task.Dealy — committed to gekiss/IdentityServer4.Contrib.MongoDB by gekiss 4 years ago
- TokenCleanupService blocked the execution of whole host (#38) * Fixes #24 added TokenCleanup hosted service * Configured token cleanup in host application. * Call Task.Yield() in TokenCleanupSe... — committed to diogodamiani/IdentityServer4.Contrib.MongoDB by gekiss 3 years ago
- Refactor Orders consumer Also, the calling thread was getting blocked until there was a message available in the topic, thus preventing the host from starting. More info below: https://github.com/dot... — committed to fredimachado/NCafe by fredimachado 2 years ago
Fixed by adding
await Task.Yield()
at the top of methodSorry 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 inStartAsync
. If the user wants to perform initialization that must complete before letting the app continue, they can overrideStartAsync
itself, right?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 newInitializeAsync
that is called andawait
ed duringStartAsync
.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 intoBackgroundService
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 inExecuteAsync
has to be promoted to mutable class state. This gets particularly ugly when NRTs are enabled.This:
Turns into this:
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 theStartAsync
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
andServicesStartConcurrently
changes in .NET 8.0 do not address this issue. The host (as currently written) still starts the services by callingStartAsync
directly. So a synchronous implementation ofStartAsync
will still block host startup.Look at the implementation of
BackgroundService
.StartAsync
is a synchronous (!!) method which returns either theTask
taken as result fromExecuteAsync
, if said taskIsCompleted
; and otherwise returns a pre-completedTask.CompletedTask
. It will take responsbility to stop the execute task inStopAsync
by triggering the cancellation token, regardless.Because
StartAsync
is synchronous, the code will block until the actualExecuteAsync
implementation, if it is an async method, does its firstawait
. That firstawait
is where the asynchronous state machine compiler magic effectively kicks in and the async method returns itsTask
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 meansBackgroundService.ExecuteAsync
won’t return tillawait
is called). So this problem is not related to ASP.NET Core, but to async compilation.I think this should make it in NET 8.0 as indicated here https://github.com/dotnet/runtime/issues/86511 We have gotten two new properties added that should resolve this.
https://learn.microsoft.com/dotnet/api/microsoft.extensions.hosting.hostoptions.servicesstartconcurrently?view=dotnet-plat-ext-8.0
https://learn.microsoft.com/dotnet/api/microsoft.extensions.hosting.hostoptions.servicesstopconcurrently?view=dotnet-plat-ext-8.0
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, theTask.Yield
workaround will do it for me.Facing same problem that background tasks is blocking main thread with
IAsyncEnumerable
. That’s probably due toIAsyncEnumerable
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 useBackgroundService.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.