I understand there's a TPL deadlock trap when calling async method within a sync MVC method, while using .Wait() or .Result to wait till the task complete.
But we just found a strange behaviour in our MVC application: The sync action calls an async method, but since it's a trigger, we never waited it complete. Still, the async method seems stucked.
Code is like below, this strange issue not 100% happens. It just happens sometime.
When it happens:
- The HomeController.Index() action completed
- Log.Info("Begin") executed.
- SaveToDb() did the job, but unknown if it hanging after complete.
- PublishTomessageQueue() doesn't do the job, unknown if it never started or just stuck inside.
- Neither Log.Info("Finish")/Log.Error("Error") been invoked.
Most of time, code works as expected.
The ISomeInterface.Trigger() also been called from other places, windows services rather than the mvc, but this odd behaviour never happens.
So my question is, would it be possible that async tasks get into deadlock even WITHOUT .Wait() nor .Result?
Many thanks.
public interface ISomeInterface
{
Task Trigger();
}
public class SomeClass
{
public async Task Trigger()
{
Log.Info("Begin");
try
{
await SaveToDb();
await PublishToMessageQueue();
Log.Info("Finish");
}
catch (Exception ex)
{
Log.Error("Error");
}
}
}
public class HomeController : Controller
{
public ISomeInterface Some { get; set; }
public ActionResult Index()
{
Some.Trigger(); //<----- The thread is not blocked here.
return View();
}
}
Yes. There's a couple of major problems with this code.
First, it can attempt to resume on a request context that no longer exists. For example, the request for
Index
comes in, and ASP.NET creates a new request context for that thread. It then invokesIndex
within that request context, andIndex
callsSome.Trigger
, and whenTrigger
hits its firstawait
, it captures that context by default and returns an incomplete task toIndex
.Index
then returns, notifying ASP.NET that the request is complete; ASP.NET sends the response and then tears down that request context. Later on,Trigger
is ready to resume after itsawait
, and attempts to resume on that request context... but it no longer exists (the request has already completed). Pandemonium ensues.The second major problem is that this is "fire and forget", which is a really bad idea on ASP.NET. It's a bad idea because ASP.NET is designed entirely around a request/response system; it has very limited facilities for working with code that does not exist as part of a request. When there are no active requests, ASP.NET can (and will) periodically recycle your app domain and worker process (this is required to keep things clean). It has absolutely no idea that your
Trigger
code is running because the request that called it has already completed - thus, your running code can just disappear periodically.The easiest solution is to move this "trigger" code into an actual request. E.g.,
Index
canawait
the task returned byTrigger
. Or have your page code issue an AJAX call to an API that callsTrigger
(andawait
s it).If this isn't doable, then I'd recommend a proper distributed system: have
Index
place a "trigger request" into a reliable queue and have it processed by an independent backend (e.g., Win32 service). Or you could use an off-the-shelf solution like Hangfire.Yes, it is possible. By default the execution will be marshaled back to the original thread after the await. But if the thread is not available or blocked for some reason a deadlock may occur.
It is not sure that this is the problem also in your case but you can try the following:
ConfigureAwait(false)
tells the runner state machine that the execution can be continued on any thread. In most cases this is alright. The marshaling back to the original thread is required only in special cases (eg. WinForms or WPF UI thread).