【问题标题】:TPL Task deadlock when calling async from sync without await in MVC在 MVC 中从同步调用异步而不等待时 TPL 任务死锁
【发布时间】:2017-08-03 07:40:24
【问题描述】:

我知道在同步 MVC 方法中调用异步方法时存在 TPL 死锁陷阱,同时使用 .Wait() 或 .Result 等待任务完成。

但是我们刚刚在我们的 MVC 应用程序中发现了一个奇怪的行为:同步操作调用了一个异步方法,但由于它是一个触发器,我们从未等待它完成。不过,异步方法似乎卡住了。

代码如下所示,这个奇怪的问题不是 100% 发生的。它只是在某个时候发生。

当它发生时:

  1. HomeController.Index() 动作完成
  2. Lo​​g.Info("Begin") 已执行。
  3. SaveToDb() 完成了这项工作,但不知道它是否在完成后挂起。
  4. PublishTomessageQueue() 没有完成这项工作,不知道它是否从未启动或只是卡在里面。
  5. 没有调用 Log.Info("Finish")/Log.Error("Error")。

大多数时候,代码按预期工作。

ISomeInterface.Trigger() 也从其他地方调用,Windows 服务而不是 mvc,但这种奇怪的行为从未发生过。

所以我的问题是,即使 没有 .Wait() 也没有 .Result,异步任务是否有可能陷入死锁?

非常感谢。

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();
    }


}

【问题讨论】:

  • 我不是很精通MVC,但我好像记得可以制作控制器方法async,例如:public async Task&lt;ActionResult&gt; Index()。那么你应该可以在Trigger方法上await
  • @easuter 是的,你是对的。这就是我所做的。我问这个问题的原因是我很好奇这种情况下怎么会发生僵局。

标签: c# asp.net-mvc asynchronous task deadlock


【解决方案1】:

异步方法似乎卡住了...它只是在某个时候发生...大多数时候,代码按预期工作。

是的。这段代码有几个主要问题。

首先,它可以尝试在不再存在的请求上下文上恢复。例如,Index 的请求进来了,ASP.NET 为该线程创建了一个新的请求上下文。然后它在该请求上下文中调用IndexIndex 调用Some.Trigger,当Trigger 遇到它的第一个await 时,它captures that context by default 并向Index 返回一个不完整的任务。 Index 然后返回,通知 ASP.NET 请求完成; ASP.NET 发送响应,然后拆除该请求上下文。稍后,Trigger 准备在其await 之后恢复,并尝试在该请求上下文上恢复......但它不再存在(请求已经完成)。混乱随之而来。

第二个主要问题是这是"fire and forget", which is a really bad idea on ASP.NET。这是一个坏主意,因为 ASP.NET 是完全围绕请求/响应系统设计的;它具有非常有限的功能来处理请求中不存在的代码。当没有活动请求时,ASP.NET 可以(并且将)定期回收您的应用程序域和工作进程(这是必需以保持清洁)。它完全不知道您的 Trigger 代码正在运行,因为调用它的请求已经完成 - 因此,您正在运行的代码可能会定期消失。

最简单的解决方案是将此“触发”代码移动到实际请求中。比如Index可以awaitTrigger返回的任务。或者让您的页面代码向调用 Trigger(和 awaits)的 API 发出 AJAX 调用。

如果这不可行,那么我会推荐一个合适的分布式系统:让Index 将“触发请求”放入可靠队列,并由独立后端(例如 Win32 服务)处理。或者您可以使用现成的解决方案,例如 Hangfire

【讨论】:

  • 第一个问题是有道理的。也许我可以尝试重现它。第二个在我的情况下不是问题,我的应用程序永远不会回收。触发器正在触发由后端服务集群处理的异步任务。这可能是一个实践问题,但这不是导致这种“僵局”或“永不返回”的原因。我已将其设为异步操作。试着了解一下“僵局”是如何发生的。谢谢
  • 经过测试,您指出的“第一个主要问题”是正确的!如果我在 write db 方法之后立即设置任务延迟,则该问题可以 100% 重现。谢谢你的回答。
  • 即发即弃仍然是一个问题,除非您不再使用即发即弃。您的代码未完成这是一个问题这一事实表明,即发即弃不正确 - ASP.NET 上的即发即弃仅适用于 您实际上并不关心它是否运行,例如,更新缓存。如果您关闭回收(这是可能的),则会导致其他长期稳定性问题。
  • 回收实际上已关闭(除了高内存使用率之外的大部分回收触发),这在 3 年后看起来还不错。它关闭的主要原因是在 Asp.NET 中运行了一个后端监视进程。虽然这个过程停止或重新启动是无害的,但如果让它继续运行会更有效率。它之所以是一劳永逸的原因是因为它只是发送一条消息,而我不在乎结果。但无论如何,现在问题已经解决,就像你建议的那样,我在响应之前等待任务完成。无论如何,它们只是触发并且不消耗时间/资源。
【解决方案2】:

即使没有 .Wait() 也没有 .Result,异步任务是否有可能陷入死锁?

是的,这是可能的。默认情况下,等待之后执行将被封送回原始线程。但如果线程不可用或由于某种原因被阻塞,则可能会发生死锁。

不确定这是否也是您的问题,但您可以尝试以下方法:

await SaveToDb().ConfigureAwait(false);

await PublishToMessageQueue().ConfigureAwait(false);

ConfigureAwait(false) 告诉运行器状态机可以在任何线程上继续执行。在大多数情况下,这没问题。只有在特殊情况下(例如 WinForms 或 WPF UI 线程)才需要封送回原始线程。

【讨论】:

  • SaveToDb 的内部方法已经调用了“ConfigureAwait(false)”。 SaveToDb() 方法只返回内部方法的任务输出。我不明白的是“......等待后将被编组回原始线程......”:“原始线程”不是一个正在运行的 Trigger() 方法吗?而且由于 Trigger() 方法是异步的,当它在“等待”时“等待”时,它的线程不应该回到线程池吗?谢谢。
  • 是的,我的意思是线程,它运行Trigger 方法作为“原始”方法。等待的方法可能在另一个线程上运行,如果没有ConfigureAwait,则Trigger的其余部分的执行将切换回与第一部分相同的线程。如果这也是一个池线程,则可能会在该线程上执行另一个任务,因此执行将被阻塞,直到它再次空闲。这是一种可能的死锁情况。使用ConfigureAwait(false),第二部分可以在任何线程上继续。
  • 感谢您的回答。所以它可能会死锁,但这个死锁与“由同步调用的异步”无关,对吧?可能这种僵局与我如何称呼它无关。
猜你喜欢
  • 1970-01-01
  • 2021-12-26
  • 1970-01-01
  • 1970-01-01
  • 2016-03-24
  • 2015-08-27
  • 1970-01-01
  • 2019-01-13
  • 1970-01-01
相关资源
最近更新 更多