干净地处理 ASP.NET 异步页面上的 AsyncTimeout

发布于 2024-08-23 21:04:17 字数 11969 浏览 2 评论 0原文

根据这篇文章

始终调用 Begin 事件处理程序

第二个含义 AsyncTimeout 我确实没有 直到最近才内化的是 的开始事件处理程序 注册的异步任务总是 调用,即使页面已计时 在 ASP.NET 开始之前发布 开始该任务。

在这种情况下,剩余的 任务基本上是注定的。页面 已经超时,但 ASP.NET 正在 仍将走走过场 调用开始事件处理程序 所有已注册的任务,均已关注 立即致电他们 相应的超时事件处理程序。

AsyncTimeout 并不意味着异步任务取消

在可能的情况下,我应该注意,一旦整个页面超时已经触发,就不要启动其他任务,并且一旦发生超时,我应该取消剩余的正在运行的任务。这是推荐/安全的吗?鉴于异步任务可能存在多个位置,我将如何彻底取消任何 HttpWebRequest.BeginGetResponse 调用?例如,如果我在 BeginGetResponse 调用中,我应该返回什么以使 IAsyncResult 安全地停止处理?相同的准则是否适用于 SqlCommand.BeginExecuteReader?

我已经将一些示例和我自己添加的示例代码放在一起:

<%@ Page Language="C#" Async="true" AsyncTimeout="30" %>

<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Threading" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    protected class RequestState
    {
        public HttpWebRequest Request;
        public string RequestID; // simple identifier of the request
        public string Url;
        public DateTime RequestStartTime;

        public RequestState(string requestID, string url)
        {
            this.RequestID = requestID;
            this.Url = url;
        }

        public void CreateRequest()
        {
            Request = (HttpWebRequest)WebRequest.Create(Url + pageDest);
            Request.Method = "GET";
            Request.Proxy = WebRequest.DefaultWebProxy;
            Request.Timeout = 1000 * 3; // Connection timeout
            Request.ReadWriteTimeout = 1000 * 15; // Read response timeout

            RequestStartTime = DateTime.Now; // Technically the request didn't start yet, but this is close enough
        }
    }

    protected class ResponseDetails
    {
        public string RequestID;
        public string Url;
        public string ServerID;
        public double Duration;
        public string ResponseString;

        public string FormattedDuration
        {
            get
            {
                return String.Format("{0:N0}ms", Duration);
            }
        }

        public bool IsServerOk
        {
            get
            {
                // trivial check, better check intended for real implementation
                return ResponseString.Contains("<body");
            }
        }

        public ResponseDetails(RequestState reqState, string serverID, string responseString)
        {
            this.RequestID = reqState.RequestID;
            this.ServerID = serverID;
            this.Url = reqState.Url;
            this.ResponseString = responseString;
            this.Duration = (DateTime.Now - reqState.RequestStartTime).TotalMilliseconds;
        }
    }

    public const string pageDest = "/";
    Dictionary<string, string> serverRequests = new Dictionary<string, string>()
    {
      { "dev1", "http://www.yahoo.com" },
      { "dev2", "http://www.msn.com" },
      { "dev3", "http://www.google.com" }
    };
    Dictionary<string, ResponseDetails> serverResponses = new Dictionary<string, ResponseDetails>();
    object dictLock = new object();
    int maxIOThreads = 0;
    int maxWorkerThreads = 0;

    protected bool ShowThreadingInfo = false;
    protected bool ShowRequestTime = true;
    protected bool ExecuteInParallel = true;

    private string GetThreadingCounts()
    {
        StringBuilder sb = new StringBuilder();

        sb.AppendFormat("<b>EndIOCPUpDate {0}</b><br />", DateTime.Now);
        /*
                sb.AppendFormat("CompletedSynchronously: {0}<br/><br/>" + AR.CompletedSynchronously + "<br /><br />");

                sb.AppendFormat("isThreadPoolThread: " + System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() + "<br />";

                sb.AppendFormat("ManagedThreadId : " + System.Threading.Thread.CurrentThread.ManagedThreadId + "<br />";

                sb.AppendFormat("GetCurrentThreadId : " + AppDomain.GetCurrentThreadId() + "<br />";

                sb.AppendFormat("Thread.CurrentContext : " + System.Threading.Thread.CurrentContext.ToString() + "<br />";
        */

        int availWorker = 0;
        int maxWorker = 0;
        int availCPT = 0;
        int maxCPT = 0;

        ThreadPool.GetAvailableThreads(out availWorker, out availCPT);
        ThreadPool.GetMaxThreads(out maxWorker, out maxCPT);

        if (maxIOThreads < (maxCPT - availCPT))
            maxIOThreads = (maxCPT - availCPT);
        if (maxWorkerThreads < (maxWorker - availWorker))
            maxWorkerThreads = (maxWorker - availWorker);

        sb.AppendFormat("--Available Worker Threads: {0}<br/>", availWorker);
        sb.AppendFormat("--Maximum Worker Threads: {0}<br/>", maxWorker);
        sb.AppendFormat("--Available Completion Port Threads: {0}<br/>", availCPT);
        sb.AppendFormat("--Maximum Completion Port Threads: {0}<br/>", maxCPT);
        sb.AppendFormat("===========================<br /><br />");

        return sb.ToString();
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        foreach (KeyValuePair<string, string> kvp in serverRequests)
        {
            RequestState reqState = new RequestState(kvp.Key, kvp.Value);

            Page.RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(this.BeginGetStatusPage),
            new EndEventHandler(this.EndGetStatusPage), new EndEventHandler(this.TimeoutHandler), reqState, ExecuteInParallel));
        }
    }

    protected override void OnPreRenderComplete(EventArgs e)
    {
        base.OnPreRenderComplete(e);
        // write the result messages to the Label.
        MessageOut();
    }

    private IAsyncResult BeginGetStatusPage(Object sender, EventArgs e, AsyncCallback cb, object state)
    {
        RequestState reqState = (RequestState)state;

        AddTraceMessage("Begin " + reqState.Url);

        reqState.CreateRequest();

        return reqState.Request.BeginGetResponse(cb, reqState);
    }

    void EndGetStatusPage(IAsyncResult asyncResult)
    {
        AddTraceMessage("EndAsync");

        if (asyncResult != null)
        {
            RequestState reqState = asyncResult.AsyncState as RequestState;

            AddTraceMessage("End " + reqState.Url);

            string serverKey = null;
            string retString = null;
            StringBuilder sb = new StringBuilder();

            try
            {
                using (WebResponse response1 = (WebResponse)reqState.Request.EndGetResponse(asyncResult))
                {
                    AddTraceMessage("End Response " + reqState.Url);

                    // grab a custom header later, not useful yet
                    serverKey = response1.Headers["Server"];

                    // we will read data via the response stream
                    using (Stream resStream = response1.GetResponseStream())
                    {
                        using (StreamReader rdr = new StreamReader(resStream))
                        {
                            sb.Append(rdr.ReadToEnd());
                            rdr.Close();
                        }
                        resStream.Close();
                    }
                    response1.Close();
                }
            }
            catch (WebException ex)
            {
                sb.AppendLine(ex.Status.ToString() + ": " + ex.Message);
            }
            retString = sb.ToString();

            AddTraceMessage("End Response2 " + reqState.Url);

            ResponseDetails rd = new ResponseDetails(reqState, serverKey, retString);

            UpdateServerResponses(rd);
        }
    }

    void UpdateServerResponses(ResponseDetails details)
    {
        lock (dictLock)
        {
            serverResponses.Add(details.RequestID, details);
        }
    }

    // This doesn't actually cancel the task I don't think
    void TimeoutHandler(IAsyncResult asyncResult)
    {
        AddTraceMessage("Request Timed Out");
        // Aborts one request running during page timeout.  What about the rest??
        RequestState reqState = asyncResult.AsyncState as RequestState;
        reqState.Request.Abort();

        ShowErrorDetails("<span style='color:red;font-weight:bold'>Timed out:</span> " + reqState.RequestID + "<br/><br/>");
    }

    private void ShowErrorDetails(ResponseDetails rd)
    {
        PanelErrors.Visible = true;
        LabelErrors.Text += String.Format("<span style='color:blue;font-weight:bold'>{0}</span><br/>{1}<br/><br/>", rd.RequestID, rd.ResponseString);
    }

    private void ShowErrorDetails(string message)
    {
        PanelErrors.Visible = true;
        LabelErrors.Text += message;
    }
    private void MessageOut()
    {
        PanelThreading.Visible = ShowThreadingInfo;

        Page.Trace.Write(_trace.ToString());

        foreach (KeyValuePair<string, string> kvp in serverRequests)
        {
            string respStr = "<font color='red'>No Reply</font>";

            if (serverResponses.ContainsKey(kvp.Key))
            {
                ResponseDetails rd = serverResponses[kvp.Key];

                if (rd.IsServerOk)
                {
                    respStr = "OK";
                    respStr += " (" + rd.ServerID + ")";
                }
                else
                {
                    ShowErrorDetails(rd);
                    respStr = "<font color='red'>ERROR</font>";
                }

                //In parallel task mode it seems to return the time of the single longest individual request.  Really weird.
                if (ShowRequestTime)
                    respStr += " [" + rd.FormattedDuration + "]";
            }

            this.Label1.Text += String.Format("{0}: {1}<br/>", kvp.Key, respStr);
        }

        this.Label1.Text += String.Format("<br/><b>MaxWorkerThreads:</b> {0}<br/>", maxWorkerThreads);
        this.Label1.Text += String.Format("<b>MaxIOThreads:</b> {0}<br/>", maxIOThreads);
    }

    StringBuilder _trace = new StringBuilder();
    DateTime _pageStartTime = DateTime.Now;

    public void AddTraceMessage(string message)
    {
        double t = (DateTime.Now - _pageStartTime).TotalSeconds;

        lock (_trace)
        {
            _trace.AppendFormat("Thread:[{0:000}] {1:00.000} -- {2}\r\n",

            System.Threading.Thread.CurrentThread.GetHashCode(), t, message);

            LabelThreading.Text += GetThreadingCounts();
        }
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label runat="server" ID="Label1"></asp:Label>
        <asp:Panel runat="server" ID="PanelErrors" Visible="false">
            <br />
            <h2>
                Error Details:</h2>
            <br />
            <asp:Label runat="server" ID="LabelErrors"></asp:Label>
        </asp:Panel>
        <asp:Panel runat="server" ID="PanelThreading" Visible="true">
            <br />
            <h2>
                Threading Details:</h2>
            <br />
            <asp:Label runat="server" ID="LabelThreading"></asp:Label>
        </asp:Panel>
    </div>
    </form>
</body>
</html>

更新:我还应该提供其他信息吗?

According to this article

The Begin Event Handler is Always Invoked

The second impliciation of
AsyncTimeout that I had not really
internalized until recently is that
the begin event handler for a
registered async task is always
invoked, even if the page has timed
out before ASP.NET gets around to
starting that task.

In this scenario, the remainder of the
tasks are basically doomed. The page
has timed out already, but ASP.NET is
still going to go through the motions
of calling the begin event handler for
all registered tasks, followed
immediately by a call to their
corresponding timeout event handlers.

AsyncTimeout Does Not Imply Asynchronous Task Cancellation

where possible I should be mindful to not kick off additional tasks once the whole page timeout has already fired and I should cancel remaining running tasks once the timeout occurs. Is this recommended/safe? How would I go about cleanly canceling any HttpWebRequest.BeginGetResponse calls given there are multiple places the async task could be? For instance if I'm inside the BeginGetResponse call, what should I return for IAsyncResult to stop processing in its tracks safely? Would the same guidelines apply to SqlCommand.BeginExecuteReader?

I've put together a sample code from a few examples and my own additions:

<%@ Page Language="C#" Async="true" AsyncTimeout="30" %>

<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Threading" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
    protected class RequestState
    {
        public HttpWebRequest Request;
        public string RequestID; // simple identifier of the request
        public string Url;
        public DateTime RequestStartTime;

        public RequestState(string requestID, string url)
        {
            this.RequestID = requestID;
            this.Url = url;
        }

        public void CreateRequest()
        {
            Request = (HttpWebRequest)WebRequest.Create(Url + pageDest);
            Request.Method = "GET";
            Request.Proxy = WebRequest.DefaultWebProxy;
            Request.Timeout = 1000 * 3; // Connection timeout
            Request.ReadWriteTimeout = 1000 * 15; // Read response timeout

            RequestStartTime = DateTime.Now; // Technically the request didn't start yet, but this is close enough
        }
    }

    protected class ResponseDetails
    {
        public string RequestID;
        public string Url;
        public string ServerID;
        public double Duration;
        public string ResponseString;

        public string FormattedDuration
        {
            get
            {
                return String.Format("{0:N0}ms", Duration);
            }
        }

        public bool IsServerOk
        {
            get
            {
                // trivial check, better check intended for real implementation
                return ResponseString.Contains("<body");
            }
        }

        public ResponseDetails(RequestState reqState, string serverID, string responseString)
        {
            this.RequestID = reqState.RequestID;
            this.ServerID = serverID;
            this.Url = reqState.Url;
            this.ResponseString = responseString;
            this.Duration = (DateTime.Now - reqState.RequestStartTime).TotalMilliseconds;
        }
    }

    public const string pageDest = "/";
    Dictionary<string, string> serverRequests = new Dictionary<string, string>()
    {
      { "dev1", "http://www.yahoo.com" },
      { "dev2", "http://www.msn.com" },
      { "dev3", "http://www.google.com" }
    };
    Dictionary<string, ResponseDetails> serverResponses = new Dictionary<string, ResponseDetails>();
    object dictLock = new object();
    int maxIOThreads = 0;
    int maxWorkerThreads = 0;

    protected bool ShowThreadingInfo = false;
    protected bool ShowRequestTime = true;
    protected bool ExecuteInParallel = true;

    private string GetThreadingCounts()
    {
        StringBuilder sb = new StringBuilder();

        sb.AppendFormat("<b>EndIOCPUpDate {0}</b><br />", DateTime.Now);
        /*
                sb.AppendFormat("CompletedSynchronously: {0}<br/><br/>" + AR.CompletedSynchronously + "<br /><br />");

                sb.AppendFormat("isThreadPoolThread: " + System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() + "<br />";

                sb.AppendFormat("ManagedThreadId : " + System.Threading.Thread.CurrentThread.ManagedThreadId + "<br />";

                sb.AppendFormat("GetCurrentThreadId : " + AppDomain.GetCurrentThreadId() + "<br />";

                sb.AppendFormat("Thread.CurrentContext : " + System.Threading.Thread.CurrentContext.ToString() + "<br />";
        */

        int availWorker = 0;
        int maxWorker = 0;
        int availCPT = 0;
        int maxCPT = 0;

        ThreadPool.GetAvailableThreads(out availWorker, out availCPT);
        ThreadPool.GetMaxThreads(out maxWorker, out maxCPT);

        if (maxIOThreads < (maxCPT - availCPT))
            maxIOThreads = (maxCPT - availCPT);
        if (maxWorkerThreads < (maxWorker - availWorker))
            maxWorkerThreads = (maxWorker - availWorker);

        sb.AppendFormat("--Available Worker Threads: {0}<br/>", availWorker);
        sb.AppendFormat("--Maximum Worker Threads: {0}<br/>", maxWorker);
        sb.AppendFormat("--Available Completion Port Threads: {0}<br/>", availCPT);
        sb.AppendFormat("--Maximum Completion Port Threads: {0}<br/>", maxCPT);
        sb.AppendFormat("===========================<br /><br />");

        return sb.ToString();
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        foreach (KeyValuePair<string, string> kvp in serverRequests)
        {
            RequestState reqState = new RequestState(kvp.Key, kvp.Value);

            Page.RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(this.BeginGetStatusPage),
            new EndEventHandler(this.EndGetStatusPage), new EndEventHandler(this.TimeoutHandler), reqState, ExecuteInParallel));
        }
    }

    protected override void OnPreRenderComplete(EventArgs e)
    {
        base.OnPreRenderComplete(e);
        // write the result messages to the Label.
        MessageOut();
    }

    private IAsyncResult BeginGetStatusPage(Object sender, EventArgs e, AsyncCallback cb, object state)
    {
        RequestState reqState = (RequestState)state;

        AddTraceMessage("Begin " + reqState.Url);

        reqState.CreateRequest();

        return reqState.Request.BeginGetResponse(cb, reqState);
    }

    void EndGetStatusPage(IAsyncResult asyncResult)
    {
        AddTraceMessage("EndAsync");

        if (asyncResult != null)
        {
            RequestState reqState = asyncResult.AsyncState as RequestState;

            AddTraceMessage("End " + reqState.Url);

            string serverKey = null;
            string retString = null;
            StringBuilder sb = new StringBuilder();

            try
            {
                using (WebResponse response1 = (WebResponse)reqState.Request.EndGetResponse(asyncResult))
                {
                    AddTraceMessage("End Response " + reqState.Url);

                    // grab a custom header later, not useful yet
                    serverKey = response1.Headers["Server"];

                    // we will read data via the response stream
                    using (Stream resStream = response1.GetResponseStream())
                    {
                        using (StreamReader rdr = new StreamReader(resStream))
                        {
                            sb.Append(rdr.ReadToEnd());
                            rdr.Close();
                        }
                        resStream.Close();
                    }
                    response1.Close();
                }
            }
            catch (WebException ex)
            {
                sb.AppendLine(ex.Status.ToString() + ": " + ex.Message);
            }
            retString = sb.ToString();

            AddTraceMessage("End Response2 " + reqState.Url);

            ResponseDetails rd = new ResponseDetails(reqState, serverKey, retString);

            UpdateServerResponses(rd);
        }
    }

    void UpdateServerResponses(ResponseDetails details)
    {
        lock (dictLock)
        {
            serverResponses.Add(details.RequestID, details);
        }
    }

    // This doesn't actually cancel the task I don't think
    void TimeoutHandler(IAsyncResult asyncResult)
    {
        AddTraceMessage("Request Timed Out");
        // Aborts one request running during page timeout.  What about the rest??
        RequestState reqState = asyncResult.AsyncState as RequestState;
        reqState.Request.Abort();

        ShowErrorDetails("<span style='color:red;font-weight:bold'>Timed out:</span> " + reqState.RequestID + "<br/><br/>");
    }

    private void ShowErrorDetails(ResponseDetails rd)
    {
        PanelErrors.Visible = true;
        LabelErrors.Text += String.Format("<span style='color:blue;font-weight:bold'>{0}</span><br/>{1}<br/><br/>", rd.RequestID, rd.ResponseString);
    }

    private void ShowErrorDetails(string message)
    {
        PanelErrors.Visible = true;
        LabelErrors.Text += message;
    }
    private void MessageOut()
    {
        PanelThreading.Visible = ShowThreadingInfo;

        Page.Trace.Write(_trace.ToString());

        foreach (KeyValuePair<string, string> kvp in serverRequests)
        {
            string respStr = "<font color='red'>No Reply</font>";

            if (serverResponses.ContainsKey(kvp.Key))
            {
                ResponseDetails rd = serverResponses[kvp.Key];

                if (rd.IsServerOk)
                {
                    respStr = "OK";
                    respStr += " (" + rd.ServerID + ")";
                }
                else
                {
                    ShowErrorDetails(rd);
                    respStr = "<font color='red'>ERROR</font>";
                }

                //In parallel task mode it seems to return the time of the single longest individual request.  Really weird.
                if (ShowRequestTime)
                    respStr += " [" + rd.FormattedDuration + "]";
            }

            this.Label1.Text += String.Format("{0}: {1}<br/>", kvp.Key, respStr);
        }

        this.Label1.Text += String.Format("<br/><b>MaxWorkerThreads:</b> {0}<br/>", maxWorkerThreads);
        this.Label1.Text += String.Format("<b>MaxIOThreads:</b> {0}<br/>", maxIOThreads);
    }

    StringBuilder _trace = new StringBuilder();
    DateTime _pageStartTime = DateTime.Now;

    public void AddTraceMessage(string message)
    {
        double t = (DateTime.Now - _pageStartTime).TotalSeconds;

        lock (_trace)
        {
            _trace.AppendFormat("Thread:[{0:000}] {1:00.000} -- {2}\r\n",

            System.Threading.Thread.CurrentThread.GetHashCode(), t, message);

            LabelThreading.Text += GetThreadingCounts();
        }
    }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Label runat="server" ID="Label1"></asp:Label>
        <asp:Panel runat="server" ID="PanelErrors" Visible="false">
            <br />
            <h2>
                Error Details:</h2>
            <br />
            <asp:Label runat="server" ID="LabelErrors"></asp:Label>
        </asp:Panel>
        <asp:Panel runat="server" ID="PanelThreading" Visible="true">
            <br />
            <h2>
                Threading Details:</h2>
            <br />
            <asp:Label runat="server" ID="LabelThreading"></asp:Label>
        </asp:Panel>
    </div>
    </form>
</body>
</html>

Update: Is there any other information I should provide?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文