Search code examples
c#async-awaitbackground-servicesystem.threading.channels

BackgroundService blocks application start until the job done


I have two background services, one to write data to a channel, and one to read data from that channel. For demo purposes, I created the channel and background services like this:

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);
        }
    }
}

Register service:

builder.Services.AddSingleton<DemoChannel>();
builder.Services.AddHostedService<Writer>();
builder.Services.AddHostedService<Reader>();

Notice that I commented a piece of code: //await Task.Delay(1, stoppingToken);

When the program is started, it must wait for the Writer to finish its loop, then the Reader will run and read all the data in the channel, and then the browser will be started. All this process is synchronous.

If I uncomment the code: //await Task.Delay(1, stoppingToken); in Writer, the program behaves exactly as I expected, i.e. Writer, and Reader are started at the same time, and the browser opens immediately without waiting. This confuses me, I don't understand why adding Delay helps the program to work properly. Is this a bug in the framework?


Solution

  • When the program is started, it must wait for the Writer to finish its loop, then the Reader will run and read all the data in the channel, and then the browser will be started. All this process is synchronous.

    The writer is fully synchronous, and I suspect that the reader and application start concurrently, with the reader likely finishing before the browser starts up just because it's faster.

    However, the writer is definitely synchronous. There are a couple of behaviors that are working together to cause this:

    1. BackgroundService.ExecuteAsync is called directly by the thread doing the host startup code, not on a separate thread pool thread. This means that the host startup will not continue until ExecuteAsync returns a Task.
    2. The channel is unbounded. This means that WriteAsync doesn't actually act asynchronously. It will asynchronously wait until space is available and then add an item, but since there's always space available, the "asynchronous wait until space is available" never actually occurs.

    The await Task.Delay code breaks this by making ExecuteAsync immediately return an incomplete task, allowing the host startup to continue.

    On my blog, I recommend wrapping ExecuteAsync in a Task.Run, which has a similar effect to await Task.Delay (but without the race condition).

    The .NET team is considering changing this behavior. Technically, it's not a bug, but it's caught enough people by surprise that they're thinking of changing it.