MaxConnectionsPerServer實驗中,發現一個過去被忽略的問題: 原來同一個Session下,啟用Session的ASP.NET網頁,因鎖定限制有可能出現單一時間內只能有一個Request被處理的情況。換句話說,即便我們使用非同步方式同時發出10個對ASP.NET網頁的Request,若該ASP.NET網頁涉及Session,這10個Request將不會同步執行,而是10個Request排成一列,一個執行完再執行下一個。

回到上回MaxConnectionsPerServer文章最後一段的案例,IE端同時丟出16個Request:

window.maxConnectionsPerServer = 16
18:43:44.316 -> 0 (234ms)
18:43:44.535 -> 16 (437ms)
18:43:44.769 -> 17 (672ms)
18:43:45.003 -> 18 (906ms)
18:43:45.237 -> 19 (1140ms)
18:43:45.877 -> 1 (1779ms)
18:43:46.423 -> 3 (2341ms)
18:43:46.938 -> 2 (2840ms)
18:43:47.484 -> 4 (3402ms)
18:43:47.999 -> 5 (3917ms)
18:43:48.514 -> 6 (4479ms)
18:43:49.029 -> 8 (5010ms)
18:43:49.544 -> 10 (5462ms)
18:43:50.059 -> 11 (5962ms)
18:43:50.574 -> 7 (6508ms)
18:43:51.090 -> 9 (7007ms)
18:43:51.605 -> 12 (7538ms)
18:43:52.120 -> 13 (8007ms)
18:43:52.635 -> 14 (8522ms)
18:43:53.150 -> 15 (9037ms)

由任兩個回應時間都至少相差0.2秒以上(這是程式故意產生的延遲),足可推論沒有任何兩個Request是同時被執行的。

MSDN Blog裡有篇文章做了詳細的探討: 當我們在一個Session中同時發出多個ASP.NET網頁Request,它們會被同步執行嗎? 答案是,可能會,可能不會。

文章提到,在ASP.NET存取任何Session物件前,它們是可以被同步執行的;但只要任何ASP.NET存取過Session物件,就會演變成每次只執行一個ASP.NET Request的局面。而這個現象源於對Session的Reader Lock/Writer Lock機制,ASP.NET網頁宣告中有個EnableSessionState Attribute,可以指定"true”、"ReadOnly"或"false"。當宣告為"true”或"ReadOnly”時,ASP.NET會在AcquireRequestState階段對Session放上Writer Lock(ReadOnly則是放Reader Lock),直到ReleaseRequestState階段才解除,幾乎涵蓋了整過ASP.NET執行周期。一旦某個ASP.NET對Session放上了Writer Lock,同一個Session的其他ASP.NET Request就必須等待放Writer Lock的網頁Request執行完解除Lock後,才有機會對Session放上自己的Reader/Writer Lock,才輪得到它執行。而Reader Lock與Writer Lock有一點不同,Reader Lock不會阻擋其他Reader Lock,但會阻擋Writer Lock;而Writer Lock則具有獨佔性,一旦Session被Writer Lock,所有的Reader Lock/Writer Lock都得乖乖在旁等待鎖定解除。

預設ASP.NET EnableSessionState="true”,所以一旦ASP.NET動用了Session,之後在每個Request執行時,都必須對Session放上獨佔性的Writer Lock,而必須獨佔Session的前題,解釋了為什麼同一時間內,永遠只有一個Request被執行。

這裡用兩個範例來觀察:

1) 為什麼存取Session物件與否會影響同步執行?
2) Reader Lock與Writer Lock間的相互影響

首先寫一支簡單ASP.NET程式:

<%@ Page Language="C#"%>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        if (Request["usesession"] == "Y")
            Session["Boo"] = "Foo";
        Response.Write("UseSession.aspx " +
            DateTime.Now.ToString("HH:mm:ss.fff"));
        Response.End();
    }
</script>

程式會在傳入?usesession=Y時隨意存取Session["Boo”],我們前後發出Request 4次,只有在第三次加上?usesession=Y,使用HttpWatch觀察(點圖檔可以檢視原圖):


在第3個Request時送出的Stream檢視左方可以看到我們首次加上usesession=Y,而當時還沒夾帶任何Cookie,而右方的回應中出現了Set-Cookie: ASP.NET_Session_Id=…,換句話說,ASP.NET的Session Cookie是在程式執行過Session[“…”]才會產生。


而如預期,之後的Request在送出時,就都會加上ASP.NET Session Cookie用以識別自己所屬的Session。一旦有了Session Cookie,ASP.NET才會衍生對Session做Writer Lock/Reader Lock的動作,這就是MSDN Blog文章中所說: 一旦你用了Session,便無法再同時執行多個Request。

接著我們寫三個使用不同Session鎖定的ASP.NET網頁:

WriterLock.aspx (EnableSessionState=”True”)

<%@ Page Language="C#" EnableSessionState="True" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        Thread.Sleep(2000);
        Response.Write("WriterLock.aspx " +
            DateTime.Now.ToString("HH:mm:ss.fff"));
        Response.End();
    }
</script>

ReaderLock.aspx (EnableSessionState=”ReadOnly”)

<%@ Page Language="C#" EnableSessionState="ReadOnly" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        Thread.Sleep(2000);
        Response.Write("ReaderLock.aspx " +
            DateTime.Now.ToString("HH:mm:ss.fff"));
        Response.End();
    }
</script>

NoLock.aspx (EnableSessionState=”False”)

<%@ Page Language="C#" EnableSessionState="False" %>
<%@ Import Namespace="System.Threading" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        Thread.Sleep(2000);
        Response.Write("NoLock.aspx " +
            DateTime.Now.ToString("HH:mm:ss.fff"));
        Response.End();
    }
</script>

以及測試用的網頁Test.htm:

<!DOCTYPE html>
 
<html>
<head>
    <title>Session Lock Test</title>
    <style>
        iframe  
        {
            width: 250px; height: 30px; margin: 10px; 
        }
    </style>
    <script type="text/javascript" 
        src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.2.js"> </script>
    <script>
        $(function () {
            var h = [];
            for (var i = 0; i < 4; i++) {
                h.push("<select id='s" + i + "'>");
                $.each(["NoLock", "ReaderLock", "WriterLock"],
                function (index, value) {
                    h.push("<option value='" + value + ".aspx'>" +
                        value + "</option>");
                });
                h.push("</select>");
            }
            h.push("<input type='button' id='go' value='GO!' />");
            $("#controller").html(h.join('\n'));
            $("#go").click(function() {
                $("select").each(function() {
                    $("#" + this.id.replace('s', 'f'))
                    .attr("src", $(this).find(":selected").val());
                });
            });
        });
    </script>
</head>
<body>
<div id='controller'></div>
<div><iframe id='f0'></iframe></div>
<div><iframe id='f1'></iframe></div>
<div><iframe id='f2'></iframe></div>
<div><iframe id='f3'></iframe></div>
</body>
</html>

由於WriterLock.aspx、ReaderLock.aspx、NoLock.aspx都會延遲兩秒後再顯示目前時間,Test.htm則可使用下拉選項選取,一次在四個IFrame中分別載入不同的ASP.NET網頁(可視為同步發出4個Request),由其顯示時間先後即可看出不同的ASP.NET網頁是同時執行或彼此阻擋。有了這個玩具,就不難驗證Reader Lock與Writer Lock理論。(註: 在執行以下測試前,我已執行過UserSession.aspx?usesession=Y,以確保Session機制被啟動)

測試1 NoLock不會彼此阻擋:

測試2 ReaderLock也不會彼此阻擋:

測試3 WriterLock擋住大家2秒,才輪到其他三個ReaderLock同步執行:

測試4 WriterLock具有獨佔性,四個WriterLock排成一列,一個執行完才跑另外一個(注意: 同時發出Request的先後,不保證其在IIS執行的順序)

最後,再證明一件事,WriterLock一定會阻止ASP.NET同時執行嗎? 不一定,前面提過,WriterLock會造成影響是因為動用到Session物件,此時會有Session Cookie指向其所屬Session物件,所以若我們將Cookie清除,就可以看到四個WriterLock同時執行囉~~~

【結論】
Session Writer Lock有可能阻礙同一Session ASP.NET網頁的同時執行,針對同Session下同步發出多個Request的情境(例如: AutoComplete輸入自動完成隨按鍵連續觸發Request、網頁內嵌多個IFrame指向多個同Session ASP.NET網頁...),可考慮透過EnableSessionState=”False”(或ReadOnly,但唯讀時仍有可能被其他WriterLock阻擋)避免鎖定效應,提升效能。而在偵察ASP.NET相關效能議題時,建議可將此一特性納入考量。


Comments

# by MerlinJY

有好有吾好

# by Walter

黑大您好,謝謝分享~ 文章中「NoLock.aspx (EnableSessionState=”Falsey”)」最後多了一個 y 字,跟您說一下 ^^

# by Jeffrey

to Walter, 已更正,謝謝提醒~~

# by huanlin

很有幫助! Thanks!

# by Anna

This is definately good content …for sure…. it's awesome to see that you are posting some unique stuff here !

# by showlife

黑大您好,針對EnableSessionState=”False”,如果有使用masterpage的頁面,是否在masterpage指定就會生效?還是要在各頁面個別指定?謝謝。

# by Jeffrey

to showlife, 查了資料, EnableSessionState是Page層次的設定,故無法套用在MasterPage上,但你可透過web.config來指定。參考: http://stackoverflow.com/questions/958687/disable-session-from-master-page

# by MerlinJY

再看这个页面时做了个小试验,发现个问题想请教下老大 关于Ajax中"Async = true"时的一个奇怪表现 连续发出两人Ajax请求后,只有一个返回了现果? Async.aspx代码: <html> <head> <title></title> <script src="../script/jquery.min.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function () { //setTimeout("Async_True('text_1', 0)", 0); //setTimeout("Async_True('text_2', 1)", 100); Async_True('text_1', 0); Async_True('text_2', 1); //setTimeout("Async_False('text_1', 0)", 0); //setTimeout("Async_False('text_2', 1)", 100); //Async_False('text_1', 0); //Async_False('text_2', 1); }); // function Async_True(text_id, num) { ajaxObj = Get_XmlHttpObject(); if (ajaxObj == null) { alert('您的浏览器不支持AJAX!'); return; } ajaxObj.onreadystatechange = function () { if (ajaxObj.readyState == 4) { if (ajaxObj.status == 200) { $('#' + text_id).text(ajaxObj.responseText); } else { alert(ajaxObj.status); } } } ajaxObj.open("POST", "Ajax.aspx?Num=" + num + "&Rand=" + Math.random(), true); ajaxObj.setRequestHeader("Content-Type", "text/xml;charset=UTF-8"); ajaxObj.send(''); } // function Async_False(text_id, num) { ajaxObj = Get_XmlHttpObject(); if (ajaxObj == null) { alert('您的浏览器不支持AJAX!'); return; } ajaxObj.open("POST", "Ajax.aspx?Num=" + num + "&Rand=" + Math.random(), false); ajaxObj.setRequestHeader("Content-Type", "text/xml;charset=UTF-8"); ajaxObj.send(''); $('#' + text_id).text(ajaxObj.responseText); } // function _Create_StandardXHR() { try { return new window.XMLHttpRequest(); } catch (e) { } } function _Create_ActiveXHR() { try { return new window.ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { } } function Get_XmlHttpObject() { var xmlHttp = null; xmlHttp = window.ActiveXObject ? _Create_ActiveXHR() : _Create_StandardXHR(); return xmlHttp; } </script> </head> <body> <div><span>1:</span><span id='text_1'></span></div> <div><span>2:</span><span id='text_2'></span></div> </body> </html> Ajax.aspx代码: <%@ Page Language="C#" %> <script type="text/C#" runat="server"> protected void Page_Load(object sender, EventArgs e) { string result = string.Empty; int i = 0; int.TryParse(Request.QueryString["Num"].ToString(), out i); DateTime t = DateTime.Now; result += t.ToLongTimeString() + " " + t.Millisecond.ToString(); if (i % 2 == 1) { System.Threading.Thread.Sleep(500); } result += "=>" + (DateTime.Now - t).Milliseconds.ToString(); //返回结果 Response.ContentType = "text/plain;charset=UTF-8"; Response.Write(result); } </script> 参考:http://www.w3school.com.cn/ajax/ajax_xmlhttprequest_send.asp

# by Jeffrey

to MerlinJY, 看了一下,我想問題出在ajaxObj = Get_XmlHttpObject();沒宣告成區域變數: 連續呼叫兩次Async_True時,第二次時會覆寫前一次設定好的ajaxObj,所以只會得到第二次的執行結果。 要解決問題,可在ajaxObj前加上var,改為區域變數就OK了! var ajaxObj = Get_XmlHttpObject();

Post a comment