Why does calling a method with await inside not block parent method`s execution?

I have read an answer to a similar question https://stackoverflow.com/a/43841624/11478903 but it doesn’t explain the yielding of execution.

I have a thread that consumes events using GetConsumingEnumerable from a BlockingCollection<Event> _eventQueue property.

public async Task HadleEventsBlocking()
{
    foreach (var event in _eventsQueue.GetConsumingEnumerable())
    {
        switch (event)
        {
            case Event.BtnAddClicked:
                HandleBtnAddClickedAsync(_cts.Token);
                break;
            case Event.BtnRemoveClicked:
                HandleBtnRemoveClickedAsync(_cts.Token);
                break;
            case Event.BtnDisableClicked:
                _cts.Cancel();
                break;
            case Event.BtnEnableClicked:
                _cts.Dispose();
                _cts = new CancellationTokenSource();
                break;
        }
        Console.WriteLine("Event loop execution complete.");
    }
}

public async Task HandleBtnAddClickedAsync(CancellationToken token)
{
    try
    {
        await Task.Run(async () =>
        {
            token.ThrowIfCancellationRequested();
            await Task.Delay(2000);
            token.ThrowIfCancellationRequested();
            Console.WriteLine("BtnAddClicked event complete");
        });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("HandleBtnAddClicked Cancelled");
    }
}
    
public async Task HandleBtnRemoveClickedAsync(CancellationToken token)
{
    try
    {
        await Task.Run(async () =>
        {
            token.ThrowIfCancellationRequested();
            await Task.Delay(2000);
            token.ThrowIfCancellationRequested();
            Console.WriteLine("BtnRemoveClicked event complete");
        });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("HandleBtnRemoveClicked Cancelled");
    }
}

And this does exactly what I want, the foreach loop executes each Event as fast as possible and does not get blocked. The methods that correspond to each Event also get the convenience of try/catch with the await Task.Run but why does this work? Because if I simply rearrange it won’t work as I want it to.

public async Task HadleEventsBlocking()
{
    foreach (var event in _eventsQueue.GetConsumingEnumerable())
    {
        switch (event)
        {
            case Event.BtnAddClicked:
                try
                {
                    await Task.Run(async () =>
                    {
                        _cts.Token.ThrowIfCancellationRequested();
                        await Task.Delay(2000);
                        _cts.Token.ThrowIfCancellationRequested();
                        Console.WriteLine("BtnAddClicked event complete");
                    });
                }
                catch (OperationCanceledException)
                {
                    Console.WriteLine("HandleBtnAddClicked Cancelled");
                }
                break;
            case Event.BtnRemoveClicked:
                try
                {
                    await Task.Run(async () =>
                    {
                        _cts.Token.ThrowIfCancellationRequested();
                        await Task.Delay(2000);
                        _cts.Token.ThrowIfCancellationRequested();
                        Console.WriteLine("BtnRemoveClicked event complete");
                    });
                }
                catch (OperationCanceledException)
                {
                    Console.WriteLine("HandleBtnRemoveClicked Cancelled");
                }
    
                break;
            case Event.BtnDisableClicked:
                _cts.Cancel();
                break;
            case Event.BtnEnableClicked:
                _cts.Dispose();
                _cts = new CancellationTokenSource();
                break;
        }
        Console.WriteLine("Event loop execution complete.");
    }
}

Now each time an event is executed the foreach loop is blocked by the await inside the try/catch and I understand why, because of the await on the Task.Run.

However I don’t understand why I get desired behavior when I pack it into a method that I don’t await. Is it because the await inside yields execution back to HandleEventsBlocking and it resumes the foreach loop? I’d also appreciate a comment on whether this is good practice, it got me far but I just don’t understand the tool I’m using it and it makes me worried.

>Solution :

However I don’t understand why I get desired behavior when I pack it into a method that I don’t await.

Because it’s not awaited. You can think of await as "asynchronous wait". It pauses the method until the returned task completes. More info on my blog: https://blog.stephencleary.com/2012/02/async-and-await.html

I’d also appreciate a comment on whether this is good practice, it got me far but I just don’t understand the tool I’m using it and it makes me worried.

Absolutely not. Ignoring tasks instead of awaiting them is dangerous: it means any exceptions are silently swallowed and your code cannot know when the processing is complete.

A better approach would be something like TPL Dataflow. Alternatively, you could create multiple consumers for your queue of work.

Leave a Reply