1

I read this article with this example:

class MyService
{
  /// <summary>
  /// This method is CPU-bound!
  /// </summary>
  public async Task<int> PredictStockMarketAsync()
  {
    // Do some I/O first.
    await Task.Delay(1000);

    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;

    // Possibly some more I/O here.
    await Task.Delay(1000);

    // More work.
    for (int i = 0; i != 10000000; ++i)
      ;

    return 42;
  }
}

It then describes how to call it depending on if you're using a UI based app or an ASP.NET app:

//UI based app:
private async void MyButton_Click(object sender, EventArgs e)
{
  await Task.Run(() => myService.PredictStockMarketAsync());
}

//ASP.NET:
public class StockMarketController: Controller
{
  public async Task<ActionResult> IndexAsync()
  {
    var result = await myService.PredictStockMarketAsync();
    return View(result);
  }
}

Why do you need to use Task.Run() to execute PredictStockMarketAsync() in the UI based app?

Wouldn't using await myService.PredictStockMarketAsync(); in a UI based app also not block the UI thread?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • 1
    because `await` on UI thrad in WinForms/WPF means "please come back to my thread whenever you need to actually run code"... – Alexei Levenkov Oct 14 '21 at 06:19
  • @MarcGravell sorry I had the wrong code in my question, I've updated it now (just the last two sentences). – David Klempfner Oct 14 '21 at 06:28
  • 1
    FWIW, the blog's author is fairly active here - I can't summon them, though :) it is also worth noting that the article there is now 8 years old, which is when `await` etc was new and in infancy (C# 5 was released August 2012); I expect our collective knowledge over things like `.ConfigureAwait(false)` was a little lower, back then – Marc Gravell Oct 14 '21 at 06:34
  • @MarcGravell Blazor (which is in some sense part of asp.net core) does have sync context though. – Evk Oct 14 '21 at 06:51
  • Related: [await Task.Run vs await](https://stackoverflow.com/questions/38739403/await-task-run-vs-await) – Theodor Zoulias Oct 14 '21 at 09:31

1 Answers1

4

By default, when you await an incomplete operation, the sync-context is captured (if present). Then, when reactivated by the completion of the async work, that captured sync-context is used to marshal the work. Many scenarios don't have a sync-context, but UI applications such as winforms and WPF do, and the sync-context represents the UI thread (basically: this is so that the default behaviour becomes "my code works" rather than "I get cross-thread exceptions when talking to the UI in my async method"). Essentially, this means that when you await Task.Delay(1000) from a UI thread, it pauses for 1000ms (releasing the UI thread) but then resumes on the UI thread. This means that you "Tons of work to do in here" happens on the UI thread, blocking it.

Usually, the fix for this is simple: add .ConfigureAwait(false), which disables sync-context capture:

await Task.Delay(1000).ConfigureAwait(false);

(usually: add this after all await in that method)

Note that you should only do this when you know that you don't need what the sync-context offers. In particular: that you aren't going to try to touch the UI afterwards. If you do need to talk to the UI, you'll have to use the UI dispatcher explicitly.

When you used Task.Run(...), you escaped the sync-context via a different route - i.e. by not starting on the UI thread (the sync-context is specified via the active thread).

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I consider the `ConfigureAwait(false)`, when used as a mechanism to offload work out of the current context, to be a hack. It doesn't guarantee that the work will be actually offloaded, and offers no control on what context the work will run. The correct way to offload work to the `ThreadPool` is the `Task.Run` IMHO. – Theodor Zoulias Oct 14 '21 at 07:03
  • 3
    @TheodorZoulias in general terms: it is `await` itself that offers no contract about where the work runs; sync-context, when present (and I think of sync-context as the unusual case, these days - although it depends what work you do), *adds* that contract, and by using `ConfigureAwait(false)` we're simply electing to suppress that, and run on whatever model the *thing being awaited* exposed to us. But in almost all cases, "let the thing being awaited dictate the activation model" is just fine (and in reality, it almost always means "on the thread pool"). – Marc Gravell Oct 14 '21 at 07:18
  • Marc and why a mechanism (`ConfigureAwait(false)`) which is empirically known to offload work to the `ThreadPool` almost always, is preferable to a mechanism (`Task.Run`) which offloads work to the `ThreadPool` always (without almost), by design and [per documentation](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run)? – Theodor Zoulias Oct 14 '21 at 07:34
  • 3
    @TheodorZoulias everything is contextual; it depends on where you are currently (what thread you're on), whether this is high-volume or low-volume (for anything low-volume: meh, don't worry about it - just go with what works), and whether the awaitable operation(s) is(are) *always* true async, *always* true sync, or something in between; as a general rule, I try to minimize *unnecessary* thread-switches (which indeed, is a design criteria of `await` in general), and `Task.Run` **forces and necessitates** a thread-switch – Marc Gravell Oct 14 '21 at 07:40
  • @TheodorZoulias `ConfigureAwait(false)` doesn't offload work. As the name sort-of-implies, [it *doesn't* set up a context switch](https://devblogs.microsoft.com/dotnet/configureawait-faq/#what-does-configureawaitfalse-do), letting the top-level caller decide whether to switch contexts or not. Execution will proceed on the original thread, or rather `TaskScheduler.Current`. There's no `almost always` which [allows the runtime to use extra optimizations](https://devblogs.microsoft.com/dotnet/configureawait-faq/#why-would-i-want-to-use-configureawaitfalse) – Panagiotis Kanavos Oct 14 '21 at 08:17
  • Marc I agreed that it's contextual. The context of this question is: *UI based app* (from the title). In this context, forcing and necessitating the responsiveness of the UI is paramount. Anything else, like avoiding a couple of thread-switches between background threads, is dwarfed in comparison. That's why I don't like the `ConfigureAwait(false)` approach. It introduces a gamble, that has high risk and low reward. – Theodor Zoulias Oct 14 '21 at 08:58
  • @MarcGravell Why not use `Task.Run()` in the ASP.NET app as well? I understand there is no UI that would be blocked, but wouldn't it mean the thread that is processing that current request, could be freed (just like the UI thread is freed), to process other HTTP requests? – David Klempfner Oct 16 '21 at 00:41
  • 1
    @DavidKlempfner no, basically; you would be using `await Task.Run(...)`, because otherwise you've introduced concurrency into the current request, which *will* cause problems - so for the current request, all you've done is add an extra thread switch (overhead); the exact same work needs to happen - you haven't gained any processing capacity by moving from one pool thread over to another pool thread (heck, you could even end up back on the same pool thread via a circuitous route) – Marc Gravell Oct 16 '21 at 09:07
  • 2
    @DavidKlempfner the main gain of `async`/`await` here is to free up the active thread whenever external work/IO happens, and we're already doing that; adding an extra thread-switch does nothing useful, but has cost. In the UI scenario, we have an additional relevant aim: *to get off the UI thread* - which justifies the cost of a thread-switch; however, this simply doesn't apply here - there are no special threads in the web scenario – Marc Gravell Oct 16 '21 at 09:10