1

I have a long-running method that I call using async/await "all the way down", starting from a button click event handler. However the UI is unresponsive for the duration of the method call, and I've found that the code is running on the UI thread. I'm probably being naive here, but I was under the assumption that await will always run the method on a b/g thread? I've been using async/await for years but this is the first time I've encountered this issue (luck maybe?!).

I found this article explaining that there are no guarantees the code won't run on the UI thread, but it doesn't seem to explain why. It's probably easier to provide examples from the article rather than my own code, as it's a bit simpler...

The author has this async method:

public static async Task LongProcessAsync()
{
    TeenyWeenyInitialization(); // Synchronous
    await SomeBuildInAsyncMethod().ConfigureAwait(false); // Asynchronous
    CalculateAndSave(); // Synchronous
}

He then goes on to say that calling the method in the following way is fine (i.e. it will always run on a b/g thread):

private async void Button1_Click(object sender, EventArgs args)
{
    await Task.Run(async () => await LongProcessAsync());
}

But calling it as seen below (which is what I use in my own code) risks having the awaited method(s) run on the UI thread:

private async void Button1_Click(object sender, EventArgs args)
{
    await LongProcessAsync();
}

I'm curious to know why this can happen. I thought the whole point of async/await was to solve the problem of keeping the UI responsive when calling long-running methods?

Edit

I think a light bulb came on in my head after reading Jakoss's answer. I've been using async/await extensively over the years, but the methods really just contain long-running synchronous code. I assumed that by decorating them with these keywords it would "automagically" run the method asynchronously, to keep the UI responsive. I suspect the only reason why it's worked up until now is that I normally call such a method from the UI tier using await Task.Run(() => SomeMethod());. Today, I've been tripped up as I've tried to use the simpler await SomeMethod();.

Andrew Stephens
  • 9,413
  • 6
  • 76
  • 152
  • 1
    Is `LongProcessAsync` actually doing any asynchronous work? If it's only doing synchronous work, it will run synchronously... – RB. Oct 13 '21 at 11:26
  • Looks like your `SomeBuildInAsyncMethod()` is sync, if you want your second ButtonClick handler work fine you must move `Task.Run()` into `LongProcessAsync()` method like `await Task.Run(() => SomeBuildInAsyncMethod());` – Ivan Khorin Oct 13 '21 at 11:30

2 Answers2

4

It's up to the method to be truly asynchronous. You can easily make synchronous method marked as async. Marking method as async does not automagically run it asynchronously. So problem you are looking for is probably inside SomeBuildInAsyncMethod. If you don't have access to the sources and you really want it to be async - run it like Task.Run(async () => await SomeBuildInAsyncMethod());. But i'd advice investigating the core issue here

Jakoss
  • 4,647
  • 2
  • 26
  • 40
  • I think this may be the answer - I don't actually have any truly asynchronous code in my methods! (See the edit on my question). So does the framework check whether any asynchronous code is present in the method's call tree, and if not, calls the code synchronously, even if the async/await keywords are present? – Andrew Stephens Oct 13 '21 at 12:07
  • @AndrewStephens async/await are only indicators for compiler to mark places when you CAN go off thread and compiler generates state machine class (https://devblogs.microsoft.com/premier-developer/dissecting-the-async-methods-in-c/) that wires everything together. But whether or not method is actually going off thread it's up to this method. `Task.Run` is a way to run CPU intensive synchronous code asynchronously, but be aware not to run a lot of long-running code this way. Default TaskScheduler have limited number of thread it's using. If you deplate those - tasks will be queued. – Jakoss Oct 13 '21 at 12:16
  • 1
    async/await has nothing to do with going off thread. These are marks to split up the method into states. The SynchronizationContext is used to handle the continuation of the next method part. (triggered by a SetResult/SetException) When no SynchronizationContext is used, the continuation is done on the threadpool. (depends on ConfigureAwait) – Jeroen van Langen Oct 13 '21 at 12:29
1

I'm curious to know why this can happen. I thought the whole point of async/await was to solve the problem of keeping the UI responsive when calling long-running methods?

AFAIK the whole point of async/await was to solve the problem of asynchronous code being extremely difficult to write, read and maintain. The async/await is a powerful tool, but not so powerful to read the developer's mind and produce the behavior that matches the developer's expectations. Also, as all powerful tools do, it allows to shoot yourself in the foot. If it didn't allow that, it wouldn't be powerful. For example it wouldn't allow us to compose our own async methods. We would be limited at using it only in async void event handlers, invoking exclusively built-in Async methods, and that's it. In which case the async-await tag would not have 21,651 questions, but probably a two-digit number at best.

So the Microsoft engineers decided to give us the power of composing our own async methods, which in turn can be composed by awaiting other async methods, and so on. When authoring async methods, the most important guideline is:

An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly.

If you follow this guideline, your async methods will be truly asynchronous. Otherwise, the ugly truth will reveal itself sooner or later, in the form of a frozen UI or a saturated ThreadPool.

Btw the linked article is composed by answers from this question.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104