-3

We have an IIS website running C# code which is compiled by IIS (this is not running from a *.cs file, it's within code with <% %> tags on a aspx page. There are no async directives used in the page code. We have a .NET library with a class that exposes async methods. When we try to call the async method synchronously, like this:

var articles = _cmsClient.GetAllArticlesAsync().Result;

the page hangs indefinitley. The solution we found by trial and error is to wrap the async call in a task:

List<Article> articles = null;
var wpLoadTask = Task.Run(async () =>
{
    articles = await _cmsClient.GetAllArticlesAsync();
});

wpLoadTask.Wait();

This is working but I'm curious to understand why.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
Paul Keister
  • 12,851
  • 5
  • 46
  • 75
  • https://www.bing.com/search?q=c%23+async+result+deadlock if one explanation is not enough. Note that your code will still lead to deadlock, just in a bit more specific conditions than standard deadlock with `.Result`. – Alexei Levenkov Mar 24 '18 at 01:01
  • @AlexeiLevenkov this look like the root cause, but this code is not experiencing a deadlock (not we're talking about deadlock, not thread starvation). Doesn't Task.Run create a new thread context that allows a new thread to be spawned for completion? If not, how is this working? – Paul Keister Mar 25 '18 at 17:15

1 Answers1

2

You should not call an asynchronous method from within a code render block (<% %>), because there's no way to make that call in an asynchronous manner. In other words, there's no such syntax such as <async% %> or anything of the sort.

Instead, set Async="true" in the @Page directive, move your async code to a codebehind method, and register it using RegisterAsyncTask in the Page_Load. This is all described in detail with samples in the documentation here.

The method you put in your codebehind should be something like this:

public async Task<List<Article>> GetAllArticlesAsync()
{
    var articles = await _cmsClient.GetAllArticlesAsync();
    return articles;
}
  • Don't use .Result or .Wait(). Those are blocking, non-asynchronous methods.

  • Don't wrap in Task.Run - that's just exchanging one task for another - which is valid for desktop apps with the UI thread, but has very little reason to be in a web app.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • To be clear, I'm not recommending this as a pattern for ASP.NET code - this is a legacy site and there is a major amount of tech debt standing in the way of making this site conform to textbook ASP.NET. It's obvious that using Task.Run isn't simply exchanging one task for another because it is allowing the call to succeed. There is some difference in the thread context of the spawned thread that allows the async call to execute. – Paul Keister Mar 23 '18 at 22:02
  • @PaulKeister with `Task.Run` you may be trading hanging site on your dev box vs. dead productions server when it gets decent load when it chews up all worker threads. – Alexei Levenkov Mar 24 '18 at 01:03
  • @AlexeiLevenkov there's no doubt that is true, which is why I am trying to understand this behavior. The Task.Run workaround increases the likelihood of thread starvation, and if someone has an alternative other than rewriting all code associated with this call to conform to a standard async pattern, I'm listening. Beyond the practice necessity of avoiding Task.Run, I would also like to understand the underlying mechanism better, specifically: why does Thread.Run() work and task.Wait() hang? – Paul Keister Mar 25 '18 at 17:04
  • 1
    @PaulKeister read articles by Stephen Cleary from linked duplicate. Summary: both cases require free *worker thread* (from thread pool) to run/finish, `.Result` (without `.ConfigureAwiat`) deadlocks because it *must* use same thread it started on; `Task.Run` deadlocks when there is no available worker thread left to either start operation or return due to inner `await`. Note that `Task.Run` is not "create new thread" (search for "C# Task vs. Thread"). – Alexei Levenkov Mar 25 '18 at 17:54
  • @AlexeiLevenkov thanks for the guidance, the linked articles do provide an explanation, which is that the SynchronizationContext for Task.Run does not capture the current context (this requires some inference, since this scenario is not explained directly). Your insistence that thread starvation and deadlock are the same is confusing to me in addition to being false. But I wish you well in any case. – Paul Keister Mar 26 '18 at 00:13