BackgroundService阻塞应用启动:添加Task.Delay为何能解决?
问题:BackgroundService同步阻塞导致启动顺序异常
我实现了两个BackgroundService:一个向Channel写入数据,另一个从该Channel读取数据。相关代码如下:
通道与服务代码
public class DemoChannel { public Channel<int> Channel { get; } public DemoChannel() { Channel = System.Threading.Channels.Channel.CreateUnbounded<int>( new UnboundedChannelOptions { SingleWriter = false, SingleReader = false, AllowSynchronousContinuations = false, }); } } public class Reader : BackgroundService { private readonly DemoChannel _channel; public Reader(DemoChannel channel) { _channel = channel; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (await _channel.Channel.Reader.WaitToReadAsync(stoppingToken)) { if (_channel.Channel.Reader.TryRead(out var series)) { Console.WriteLine("Read: " + series); } } } } public class Writer : BackgroundService { private readonly DemoChannel _channel; public Writer(DemoChannel channel) { _channel = channel; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { //await Task.Delay(1, stoppingToken); for (var i = 0; i < 100000; i++) { Console.WriteLine("------Write: " + i); await _channel.Channel.Writer.WriteAsync(i, stoppingToken); } } }
服务注册代码
builder.Services.AddSingleton<DemoChannel>(); builder.Services.AddHostedService<Writer>(); builder.Services.AddHostedService<Reader>();
现象
- 注释掉
//await Task.Delay(1, stoppingToken);时:程序启动后会先等待Writer完成100000次循环,Reader才会开始读取数据,浏览器也需要等到循环结束才会启动,全程同步执行。 - 取消注释后:Writer与Reader同时启动,浏览器立即打开,符合预期。
想知道这是否是框架Bug?
解答
这不是框架Bug,核心原因是异步方法中的同步阻塞导致的线程调度问题,具体分析如下:
无界Channel的
WriteAsync特性
无界Channel不会因为容量满而阻塞写入,所以WriteAsync方法会同步完成——调用后会立即返回已完成的Task,不会让出当前线程。无await时的同步阻塞逻辑
当Writer的ExecuteAsync开头没有await Task.Delay(1)时,整个循环会在当前线程上同步执行到底:- 循环中的打印和
WriteAsync都是同步完成的操作,没有任何真正的异步等待会让出线程。 - ASP.NET Core启动HostedService时,会依次调用各个服务的
ExecuteAsync方法,第一个服务的ExecuteAsync如果一直同步占用线程,后续的Reader服务启动、应用初始化(比如浏览器打开)都会被卡住,直到这个同步循环执行完毕。
- 循环中的打印和
加入
await Task.Delay(1)后的变化await Task.Delay(1)会让ExecuteAsync方法立即返回一个未完成的Task,主动让出当前线程:- Host启动流程得以继续,会接着启动Reader服务,同时完成应用的初始化(比如打开浏览器)。
- 当Delay完成后,Writer的循环会在线程池的空闲线程上继续执行,此时Reader已经在运行,两者就能并行处理Channel的数据了。
补充提示
这是异步编程中常见的“假异步”问题——即使方法标记为async,如果内部没有真正的异步等待操作,依然会同步阻塞线程。如果需要避免启动时的阻塞,即使是无界Channel,也需要确保ExecuteAsync尽早让出线程,比如加入短暂的await,或者将循环逻辑放到异步上下文里。
内容的提问来源于stack exchange,提问作者Minh Giang
相关产品推荐
相关产品推荐

