Search code examples
c#.net-coreasync-awaityield

Explain await Task.Yield in an ASPNET Core WebApi context


From the documentation

You can use await Task.Yield(); in an asynchronous method to force the method to complete asynchronously. If there is a current synchronization context (SynchronizationContext object), this will post the remainder of the method's execution back to that context. However, the context will decide how to prioritize this work relative to other work that may be pending.

So I'm expecting that, in a WebApi, I can make use of this in two ways

  • My async operation really needs to be async and I don't want the internal implementation to return using a synchronous path.
  • My async operation is about to do some heavy work but I don't need to block the response to the caller. As a process, I'm "giving up" my execution to the caller and I'm happy to be scheduled later.

But I'm not finding that this is happening my my .NET Core WebApi

    [HttpGet(Name = "GetWeatherForecast")]
    public async Task<IEnumerable<WeatherForecast>> Get()
    {
        _logger.LogInformation("GET: Start DoSomethingAsync");    
        await DoSomethingAsync();       
        _logger.LogInformation("GET: End DoSomethingAsync");

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }

    private async Task DoSomethingAsync()
    {
        _logger.LogDebug("DoSomethingAsync: Starting");
        await Task.Yield();
        await Task.Delay(1000);
        _logger.LogDebug("DoSomethingAsync: Finsihed");
    }    

I'm expecting "End DoSomethingAsync" to get logged first and "DoSomethingAsync: Completed" to come a second later. I know SynchronizationContext.Current is null in this case but should my DoSomethingAsync not log AFTER GetWeatherForecast is run?

dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
      Hosting started
info: AsyncTestsApi.Controllers.WeatherForecastController[0]
      Start DoSomethingAsync
dbug: AsyncTestsApi.Controllers.WeatherForecastController[0]
      DoSomethingAsync: Starting
dbug: AsyncTestsApi.Controllers.WeatherForecastController[0]
      DoSomethingAsync: Completed
info: AsyncTestsApi.Controllers.WeatherForecastController[0]
      End DoSomethingAsync

Solution

  • I'm expecting "End DoSomethingAsync" to get logged first and "DoSomethingAsync: Completed" to come a second later

    That is an incorrect expectation; the await here:

    _logger.LogInformation("GET: Start DoSomethingAsync");    
    await DoSomethingAsync();       
    _logger.LogInformation("GET: End DoSomethingAsync");
    

    means that the 3rd line doesn't happen until everything in DoSomethingAsync() has completed asynchronously if needed. You can get the behaviour you want via:

    _logger.LogInformation("GET: Start DoSomethingAsync");    
    var pending = DoSomethingAsync();       
    _logger.LogInformation("GET: End DoSomethingAsync");
    await pending;
    

    however, this is fundamentally the same as adding a second thread in regular non-async code - you now have two execution flows active on the same request. This can be valid, but can also be dangerous if thread-safety semantics are not observed.