ASP.NET Web アプリで Session を利用する場合、同一ユーザー(= SessionID が同じユーザー)が同時にアクセスすると、最初の要求に受けた時点で Session へのアクセスがロックされるので、最初の要求に対する応答が返されるまで次の要求は待たされるという話を書きます。
(元の話は MSDN フォーラムのスレッド「非同期ポストバック中のキャンセルとその後のポストバックについて」です)

SessionStateModule によるロックメカニズムはマイクロソフト公式解説書「プログラミング Microsoft ASP.NET 4」という本の 17.2.1 章「セッション状態 HTTP モジュール」の「セッション状態へのアクセスの同期」というセクションに詳しく書いてあります。その一部を抜粋すると:
"セッション状態モジュールはリーダー / ライター方式のロックメカニズムを実装し、状態値へのアクセスをキューで管理します。セッション状態への書き込みアクセスを許可されたページは、リクエストの処理が終了するまで、そのセッションのライターロックを保持します。 ・・・(中略)・・・ セッション状態への読み取りアクセスを許可されたページは、リクエストの処理が終了するまでセッションのリーダーロックを保持します"
上記の通り、Session を使うケースでは、応答が返されるまで Session へのアクセスはロックされ、その間次の要求は処理されないのですが、普通に同期ポストバックを行っている限り、それにユーザーが気が付くことはなさそうです。
ところが、ScriptManager + UpdataPanel を利用して非同期ポストバックで要求を出すようにし、それにキャンセル機能を実装した場合は話が違ってきます。
ユーザーが、処理に長い時間がかかる「要求 A」を出したが、応答が返ってくるのを待ちきれないので、一旦その要求をキャンセルして、処理に時間のかからない(=すぐ応答が返ってくる)「要求 B」を出すというケースを考えてみてください。
ユーザーは、「要求 A」はキャンセルしたのだから、「要求 B」はすぐ処理されて応答が返ってくると期待するはずです。
ところが Session を利用しているアプリの場合はユーザーの期待通りにはなりません。「要求 A」をキャンセルする / しないにかかわらず、「要求 B」の応答が返ってくるのは、普通に「要求 A」を処理する時間だけ待たされた後になります。
ユーザーが「要求 A」をキャンセルしてもサーバー側での実行が中断されるわけではなく、サーバーは「要求 A」を処理して応答を返すところまで行うということがポイントです。
Session を利用していない場合、「要求 B」をサーバーが受けると、先に受けた「要求 A」の処理と並行して、直ちに「要求 B」の処理が始まって応答が返されます。結果、ユーザーの期待通り、処理に時間のかからない「要求 B」の応答はすぐ返ってきます。
ところが、Session を利用している場合、Session ロックによって、「要求 A」の処理が終わって応答が返されるまで、「要求 B」の処理は待たされます。結果、「要求 A」の処理のキャンセルが効いてないように見えます。
下の画像を見てください。この記事の下の方に示したサンプルコードを実行し、Fiddler で要求・応答をキャプチャしたものです。

実行手順は以下の通りです。
-
[非同期]ボタンをクリックして非同期ポストバックによる「要求 A」をかける。(この記事の一番上の画像がその時のもの)
-
[Cancel]ボタンをクリックして「要求 A」をキャンセル。
-
[同期]ボタンをクリックして「要求 B」をかける。
上の Fiddler のキャプチャ画像で、キャンセルした上記 1 の要求もサーバー側では正しく処理され、完全な応答が返ってきているのが分かるでしょうか? (ただし、ブラウザで abort されています)
問題のページで Session に書き込んでいる場合は何ともならないですが、そうでない場合は以下の解決策で対応できるはずです。
-
他のページで Session を使っているが、問題のページでは使ってなければ EnableSessionState を False に設定する。
-
問題のページも Session を使っているが、読み取りのみの場合は EnableSessionState を ReadOnly に設定する。
なお、自分では Session は使っていないつもり(コードで明示的に Session["Data"] = xxx のようなことはしていない)でも、Global.asax に Session_Start ハンドラがあるとすべてのページで Session を使っているのと同じことになります。
Visual Studio のテンプレートを使ってプロジェクトを作ると、Global.asax に空の Session_Start ハンドラが生成されることがありますので注意してください。
最後に、この記事を書くときに検証用に使ったサンプルコードを以下に書いておきます。
<%@ Page Language="C#" EnableSessionState="ReadOnly" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Button_Click(object sender, EventArgs e)
{
System.Threading.Thread.Sleep(5000);
}
protected void Button2_Click(object sender, EventArgs e)
{
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script type="text/javascript">
//<![CDATA[
var manager;
function pageLoad(sender, args) {
if (args.get_isPartialLoad() === false) {
manager = Sys.WebForms.PageRequestManager.getInstance();
manager.add_initializeRequest(OnInitializeRequest);
manager.add_endRequest(OnEndRequest);
}
}
function OnInitializeRequest(sender, args) {
$get("<%=UpdatePanel1.ClientID%>").style.display = "none";
}
function OnEndRequest(sender, args) {
$get("<%=UpdatePanel1.ClientID%>").style.display = "block";
}
function AbortPostBack() {
if (manager.get_isInAsyncPostBack()) {
manager.abortPostBack();
}
}
//]]>
</script>
<style type="text/css">
#UpdatePanel1 {
width: 200px;
height: 200px;
border: gray 2px solid;
position: relative;
float: left;
margin-left: 10px;
margin-top: 10px;
}
#UpdateProgress1 {
width: 400px;
background-color: #FFC080;
border: gray 1px solid;
/*bottom: 0%;
left: 0px;
position: absolute;*/
}
</style>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:Button ID="Button1" runat="server"
Text="非同期" OnClick="Button_Click" />
<asp:Button ID="Button2" runat="server"
Text="同期" OnClick="Button2_Click" />
<br />
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
UpdatePanel
<hr />
<%=DateTime.Now.ToString() %> <br />
<br /><br />
[非同期]ボタンをクリックすると、
5 秒後にこのパネル内が更新されます。
その間 UpdateProgress が表示されます。
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1"
EventName="Click">
</asp:AsyncPostBackTrigger>
</Triggers>
</asp:UpdatePanel>
<asp:UpdateProgress ID="UpdateProgress1" runat="server">
<ProgressTemplate>
非同期ポストバックで更新中です・・・
<input type="button" value="Cancel"
onclick="AbortPostBack()" />
</ProgressTemplate>
</asp:UpdateProgress>
</form>
</body>
</html>