TPL Task deadlock when calling async from sync wit

2019-07-10 05:19发布

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:

  1. The HomeController.Index() action completed
  2. Log.Info("Begin") executed.
  3. SaveToDb() did the job, but unknown if it hanging after complete.
  4. PublishTomessageQueue() doesn't do the job, unknown if it never started or just stuck inside.
  5. 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();
    }


}

2条回答
你好瞎i
2楼-- · 2019-07-10 06:01

the async method seems stucked... It just happens sometime... Most of time, code works as expected.

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 invokes Index within that request context, and Index calls Some.Trigger, and when Trigger hits its first await, it captures that context by default and returns an incomplete task to Index. 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 its await, 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 can await the task returned by Trigger. Or have your page code issue an AJAX call to an API that calls Trigger (and awaits 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.

查看更多
你好瞎i
3楼-- · 2019-07-10 06:02

would it be possible that async tasks get into deadlock even WITHOUT .Wait() nor .Result?

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:

await SaveToDb().ConfigureAwait(false);

await PublishToMessageQueue().ConfigureAwait(false);

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).

查看更多
登录 后发表回答