当通过 PageMethods 从 javascript aspx 页面调用时,C# 中的异步函数死锁

Async functions in C# deadlock when called from javascript aspx page via PageMethods

问题总结:
我正在尝试使用 PageMethods 从 HTML 页面调用 C# 函数。问题是我正在调用的 C# 函数被标记为异步并将等待其他函数的完成。当 PageMethods 调用嵌套的异步 C# 函数时,C# 代码似乎死锁了。
我给出了一个示例 ASP.NET 页面,其背后使用 C# 编码来说明我正在尝试使用的习语。

示例WebForm1.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication3.WebForm1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title></title></head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true"/>
        <div>
            <input type="button" value="Show Function timing" onclick="GetTiming()"/>
        </div>
    </form>
</body>

<script type="text/javascript">
    function GetTiming() {
        console.log("GetTiming function started.");
        PageMethods.GetFunctionTiming(
            function (response, userContext, methodName) { window.alert(response.Result); }
        );
        console.log("GetTiming function ended."); // This line gets hit!
    }
</script>

</html>

示例WebForm1.aspx.cs

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Web.Services;
using System.Web.UI;
namespace WebApplication3
{
    public partial class WebForm1 : Page
    {
        protected void Page_Load(object sender, EventArgs e) { }
        [WebMethod]
        public static async Task<string> GetFunctionTiming()
        {
            string returnString = "Start time: " + DateTime.Now.ToString();
            Debug.WriteLine("Calling to business logic.");

            await Task.Delay(1000); // This seems to deadlock
            // Task.Delay(1000).Wait(); // This idiom would work if uncommented.

            Debug.WriteLine("Business logic completed."); // This line doesn't get hit if we await the Task!
            return returnString + "\nEnd time: "+ DateTime.Now.ToString();
        }
    }
}

问题:
我绝对需要能够从我的网页 UI 调用异步代码。我想使用 async/await 功能来执行此操作,但我一直无法弄清楚如何操作。我目前正在通过使用 Task.Wait()Task.Result 而不是 async/await 来解决这个问题,但这显然不是推荐的长期解决方案。
如何在 PageMethods 调用的上下文中等待服务器端异步函数???
我真的非常想了解这里的幕后发生了什么,以及为什么当从控制台应用程序调用异步方法时它不会发生。

这是因为 await 默认情况下会捕获当前 SynchronizationContext 并在完成后尝试 post 返回它。这与 ASP.NET 线程细节有关,但简而言之,死锁实际上是在阻塞线程的方法之间,而继续尝试 post 返回它。

有更好的设计,但解决方法或破解方法是不要尝试 post 返回捕获的 SynchronizationContext。请注意,这是一个 hack,因为它会 运行 在恰好执行 Task 的任何线程上继续(方法的其余部分)。这通常是一个 ThreadPool 线程。

不过,这将解决您的僵局。请牢记其中的危险,我会建议更好的设计。

await Task.Delay(1000).ConfigureAwait(false);

我找到了一种方法,允许 PageMethods 在 ASP.NET 上调用嵌套异步函数而不会出现死锁。我的方法涉及

  1. 使用 ConfigureAwait(false) 这样我们就不会强制异步函数尝试 return 到原始捕获的上下文(Web UI 线程将锁定);和
  2. 强制 "top level" 异步函数进入线程池,而不是将其 运行 置于 ASP.NET 的 UI 上下文中。

这两种方法中的每一种通常都在论坛和博客上被推荐反对,所以我确信 两者 都构成反模式。但是,它确实允许一个有用的垫片通过使用 PageMethods 从 ASP.NET 网页调用异步函数。
下面给出了示例 C# 代码。

示例WebForm1.aspx.cs

using System;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Web.Services;
using System.Web.UI;

namespace WebApplication3
{
    public partial class WebForm1 : Page
    {

        protected void Page_Load(object sender, EventArgs e) { }

        [WebMethod]
        public static async Task<string> GetFunctionTiming()
        {
            Debug.WriteLine("Shim function called.");
            string returnString = "Start time: " + DateTime.Now.ToString();

            // Here's the idiomatic shim that allows async calls from PageMethods
            string myParameter = "\nEnd time: "; // Some parameter we're going to pass to the business logic
            Task<string> myTask = Task.Run( () => BusinessLogicAsync(myParameter) ); // Avoid a deadlock problem by forcing the task onto the threadpool
            string myResult = await myTask.ConfigureAwait(false); // Force the continuation onto the current (ASP.NET) context

            Debug.WriteLine("Shim function completed.  Returning result "+myResult+" to PageMethods call on web site...");
            return returnString + myResult;
        }

        // This takes the place of some complex business logic that may nest deeper async calls
        private static async Task<string> BusinessLogicAsync(string input)
        {
            Debug.WriteLine("Invoking business logic.");
            string returnValue = await DeeperBusinessLogicAsync();
            Debug.WriteLine("Business logic completed.");
            return input+returnValue;
        }

        // Here's a simulated deeper async call
        private static async Task<string> DeeperBusinessLogicAsync()
        {
            Debug.WriteLine("Invoking deeper business logic.");
            await Task.Delay(1000); // This simulates a long-running async process
            Debug.WriteLine("Deeper business logic completed.");
            return DateTime.Now.ToString();
        }
    }
}