1

I have an ASP.NET MVC async action method which looks like this:

public async Task<ActionResult> IndexAsync()
{
    var tasks = new List<Task<string>>
    {
        GetSomethingAsync("a"),
        GetSomethingAsync("b")
    };

    await Task.WhenAll(tasks);

    return View();
}

private async Task<string> GetSomethingAsync()
{
    var data = await _someService.GetSomethingAsync().ConfigureAwait(false);
    return data.SomeData;
}

Now, when i debug and step over the tasks variable creation, the tasks are executed immediately. In other words, when i hover over tasks in the await line, they say "RanToCompletion".

Why?

From my understanding, the tasks should be created, but be in the "WaitingForActivation" state until triggered by the await Task.WhenAll(tasks) blocking call.

Can someone explain to me what's going on? I've written code like this before and it normally works as expected, so i'm wondering if this is an ASP.NET or ASP.NET MVC async controller thing?

TIA.

EDIT If i change the code to:

var tasks = new List<Task<string>>
{
   Task.Run(() => GetSomethingAsync("a")),
   Task.Run(() => GetSomethingAsync("b"))
};

The method runs as expected (tasks not executed until await).

I've generally never needed to do this before when running async tasks, is this needed in ASP.NET MVC ?

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • 1
    I bet you ` _someService.GetSomethingAsync()` is synchronous... – Alexei Levenkov Apr 06 '17 at 01:26
  • ConfigureAwait(false) causes the await to launch the task outside the current context – Sten Petrov Apr 06 '17 at 01:30
  • 1
    @AlexeiLevenkov ah, you're right. It's "fake" async/await. The signatures are all async/await, but the underlying code is actually a synchronous REST call. That's why... because there's no actual Task being returned. :/ – RPM1984 Apr 06 '17 at 01:30
  • @StenPetrov yes, but that's just related to the captured context, not the execution pattern, right? Shouldn't matter. – RPM1984 Apr 06 '17 at 01:33

2 Answers2

3

Per your comment you actually did not have any real asynchronous code - so indeed task will return synchronously and be in completed state.

The easiest way to make method true async is await Task.Yield(). This is fine for unit testing or methods that have to be async for some reason but don't consume much time. If you need to run slow (blocking or just CPU-intensive) methods - Task.Run as you have in the question is a reasonable way to make task running on separate thread.

Notes

  • marking method async does not by itself make it asynchronous nor await creating any threads by itself.
  • it is better to use real async methods for network calls. ASP.Net has limited thread pull and consuming threads for blocking calls will exhaust the pull under load leading to deadlocks as await'ing method will not be able to find thread to run on.
  • using ConfigureAwait(false) does not prevent load-based deadlocks in ASP.Net and conveniently looses HttpContext.Current and thread's CultureInfo - be careful to use it in ASP.Net especially that there is basically no upside to do so (cost of restoring context on new thread is very low compared to WPF/WinForm cross-thread invocation).
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
2

Now, when i debug and step over the tasks variable creation, the tasks are executed immediately. In other words, when i hover over tasks in the await line, they say "RanToCompletion".

I recommend you read my async intro. Quote:

The beginning of an async method is executed just like any other method. That is, it runs synchronously until it hits an “await” (or throws an exception).

Thus, if you have stubbed-out asynchronous methods that return completed tasks, then your method calls (e.g., GetSomethingAsync("a")) will be completed synchronously. The task is already completed by the time it is added to the list.

From my understanding, the tasks should be created, but be in the "WaitingForActivation" state until triggered by the await Task.WhenAll(tasks) blocking call.

WaitingForActivation is an unfortunate name. For Promise Tasks, the WaitingForActivation state means it's actually already in progress.

There is no "triggering" that needs to take place by Task.WhenAll or await. The task created by Task.Run(() => GetSomethingAsync("a")) is already going.

This can be observed in the debugger by inserting an await Task.Delay(1000); before the await Task.WhenAll, and checking the state of the tasks after that delay.

is this needed in ASP.NET MVC?

No. In fact, you should avoid Task.Run on ASP.NET. Quote from my MSDN article on async ASP.NET:

You can kick off some background work by awaiting Task.Run, but there’s no point in doing so. In fact, that will actually hurt your scalability by interfering with the ASP.NET thread pool heuristics. If you have CPU-bound work to do on ASP.NET, your best bet is to just execute it directly on the request thread. As a general rule, don’t queue work to the thread pool on ASP.NET.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810