WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

SessionStateModule によるロック

by WebSurfer 2018年4月6日 18:21

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 で要求・応答をキャプチャしたものです。

Fiddler によるキャプチャ

実行手順は以下の通りです。

  1. [非同期]ボタンをクリックして非同期ポストバックによる「要求 A」をかける。(この記事の一番上の画像がその時のもの)
  2. [Cancel]ボタンをクリックして「要求 A」をキャンセル。
  3. [同期]ボタンをクリックして「要求 B」をかける。

上の Fiddler のキャプチャ画像で、キャンセルした上記 1 の要求もサーバー側では正しく処理され、完全な応答が返ってきているのが分かるでしょうか? (ただし、ブラウザで abort されています)

問題のページで Session に書き込んでいる場合は何ともならないですが、そうでない場合は以下の解決策で対応できるはずです。

  1. 他のページで Session を使っているが、問題のページでは使ってなければ EnableSessionState を False に設定する。
  2. 問題のページも 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>

Tags: ,

ASP.NET

非同期 HTTP ハンドラ (2)

by WebSurfer 2018年3月8日 18:56

先の記事「非同期 HTTP ハンドラ」で、Web サービスに非同期でアクセスする HTTP ジェネリックハンドラのサンプルを書きました。

その HTTP ハンドラは IHttpAsyncHandler インターフェイスを継承して BeginProcessRequest, EndProcessRequest メソッドを実装するという旧来の方法を使っており、記事に書いてありますように非常に複雑なコードになります。

ここまで複雑なことをしなければならないのなら、HTTP 503 エラー (Server Too Busy) が頻発して困っているというような事情がなければ、従来通り同期版のままでもいいかなって気もします。(笑)

ですが、.NET 4.5 から簡単に非同期呼び出しが実装できるようになったそうです。どのぐらい簡単にできるのか、同じ機能を async / await を利用して実装してみました。

非同期 HTTP ハンドラ

確かにはるかに簡単でした。前のように、何がどうなっているのかを調べて、悩んで、時間をかけて実装するという手間は大幅に減っています。

上の画像は、この記事に書いた方法で作成した HTTP ハンドラを、ブラウザから直接呼び出した結果です。具体的にどのように実装したかは以下に書きます。

HelloWorldWcfService.cs

先の記事の Web サービスは WCF サービスに変更しました。HelloWorld メソッドの実装は先の記事の Web サービスのものと全く同じです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.ServiceModel.Activation;

[AspNetCompatibilityRequirements(RequirementsMode =
            AspNetCompatibilityRequirementsMode.Allowed)]
public class HelloWorldWcfService : IHelloWorldWcfService
{
    public string HelloWorld(int callDuration)
    {
        Thread.Sleep(callDuration);
        return String.Format(
          "Hello World from WcfService. Call Time: {0}",
          callDuration);
    }
}

WCF サービスに変更したのは、Web サービスが Legacy Technology だからということもありますが、一番の理由はサービス参照の追加を行うと自動生成されるサービスプロキシに含まれる非同期版メソッドが利用できるからです。

(非同期版メソッドについて、詳しくは先に記事「WCF サービスの非同期呼び出し」を見てください)

HelloWorldAsyncHnadler.ashx

Visual Studio で、既存の ASP.NET Web Forms アプリにジェネリックハンドラーを追加します。.ashx ファイルには何も手を加える必要はありません。

<%@ WebHandler Language="C#"
    CodeBehind="HelloWorldAsyncHandler.ashx.cs"
    Class="WebFormsApp.HelloWorldAsyncHandler" %>

Web アプリケーションプロジェクトの場合、.ashx ファイルとそのコードビハインドの .ashx.cs ファイルは別になります。Web サイトプロジェクトの場合、すべて .ashx ファイルに含まれるという違いがあります。

HelloWorldAsyncHandler.ahsx.cs

自動生成されたコードは同期版ハンドラのベースとなるものです。これを HttpTaskAsyncHandler クラスを継承したクラスに書き換えます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using WebFormsApp.HelloWorldServiceReference;

namespace WebFormsApp
{
    public class HelloWorldAsyncHandler : HttpTaskAsyncHandler
    {
        HelloWorldWcfServiceClient client;

        // これが呼び出されることはない。万一呼び出されたら例外
        // をスローして自爆する
        public override void ProcessRequest(HttpContext context)
        {
            throw new InvalidOperationException();
        }
        
        public override async Task ProcessRequestAsync(
                                            HttpContext context)
        {
            context.Response.Write(
                "<p>Before await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            client = new HelloWorldWcfServiceClient();

            string result = await client.HelloWorldAsync(3000);

            context.Response.Write(
                "<p>After await:<br />" +
                " IsThreadPoolThread is " +
                Thread.CurrentThread.IsThreadPoolThread +
                "<br />" +
                " ManagedThreadId is " +
                Thread.CurrentThread.ManagedThreadId +
                "</p>");

            context.Response.Write("<p>" + result + "</p>");
        }

        public override bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

上のコードの WebFormsApp.HelloWorldServiceReference はサービスプロキシの名前空間です。HelloWorldWcfServiceClient コンストラクタ、HelloWorldAsync メソッドの定義は自動生成される Reference.cs ファイルにあります。

結局は先の記事「非同期 HTTP ハンドラ」と同様なことを行っていて、先の記事のコードの BeginProcessRequest メソッドが await キーワードの前に、EndProcessRequest メソッドが await の後に実行されているようです。

BeginProcessRequest, EndProcessRequest メソッドをプログラマがコードを書いて実装しなくて済むよう、コンパイラが肩代わりしているという感じです。

Tags: , ,

ASP.NET

WCF サービス経由で非接続型データアクセス

by WebSurfer 2018年3月4日 14:41

型付 DataSet + TableAdapter を用いて非接続型データアクセスを行う Windows Forms アプリケーションで、WCF サービス経由で SQL Server のデータの取得更新を行う例の紹介です。(Windows Forms アプリから直接 SQL Server に接続するのではなく)

DataGridView で表示

接続先を Web サービスとした場合の例は先の記事「Web サービス経由で非接続型データアクセス」に書きました。

Microsoft によると、Web サービスは "Legacy Technology" なので Windows Communication Foundation (WCF) を使うようにとのことです。なので、この記事では、Web サービスに代えて WCF サービスを使い、さらにサービス参照を作成する際に生成される非同期版メソッドを使って、非同期にアクセスするサンプルを作ってみました。

Visual Studio のデータソース構成ウィザードを使うと、MSDN ライブラリの Windows フォーム アプリケーションでのデータへの接続 の図のような構成の Windows アプリケーションを作成できます。具体的な作成手順例は、チュートリアル「10 行でズバリ !! 非接続型のデータ アクセス」を見てください。

上に紹介したチュートリアルの例では Windows アプリから直接 SQL Server に接続していますが、セキュリティ上の問題などで、Windows アプリ ⇔ サーバー ⇔ SQL Server というようにサーバーを介してアクセスしたいということがあると思います。

Web サーバー (IIS) 上で動いている ASP.NET Web Forms アプリケーションに WCF サービスを実装し、Windows Forms アプリケーションとの間で型付 DataSet をやりとりすることで Windows アプリ ⇔ Web サーバー ⇔ SQL Server という構成を実現できます。

(1) WCF サービス

まず WCF サービスを IIS 上で動いている既存の ASP.NET Web Forms アプリに実装します。Visual Studio の「ソリューションエクスプローラー」から[追加(D)]⇒[新しい項目の追加(W)...]で表示される「新しい項目の追加」ダイアログで[WCF サービス]を選択して追加します。

WCF サービスの追加

ここでは Web サイトプロジェクトのアプリケーションルート直下に WCF サービスを追加し、その名前を ProductsService.svc とするという前提で話を進めますので留意してください。

追加すると、IProductsService.cs, ProductsService.cs が App_Code フォルダに、アプリケーションルート直下に ProductsService.svc が生成されます。(Web アプリケーションプロジェクトでは生成されるファイルの場所と名前が違いますが基本は同じです)

次に、Visual Studio のデータソース構成ウィザードを使って、SQL Server のサンプルデータベース Northwind の Products テーブルをベースに、型付 DataSet + TableAdapter + TableAdapterManager を生成します。ここでは、名前を ProductsDataSet.xsd としています。

その手順は割愛しますが、一つだけ注意点を書くと、Web サイトプロジェクトの場合、.xsd ファイルは App_Code フォルダに作るのではなく、同一ソリューション内に別プロジェクトを作ってそこに作るようにした方がよさそうです。Temporary ASP.NET Files フォルダに生成される .Designer.cs ファイルのコードが読めないという不都合のほか、自分の環境ではなぜか TableAdapterManager が生成されないという問題がありましたので。

以上が完了したら、(a) SQL Server から型付 DataSet にデータを取得して Windows Forms アプリケーション渡すメソッド、(b) ユーザーが編集済みの型付 DataSet を受け取って SQL Server のレコードを更新するメソッドを ProductsService.cs に実装します。

具体例は以下のようになります。GetDataSet メソッドが上記 (a)、Update メソッドが上記 (b) に該当します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using NorthwindProducts;
using NorthwindProducts.ProductsDataSetTableAdapters;

[AspNetCompatibilityRequirements(RequirementsMode =
            AspNetCompatibilityRequirementsMode.Allowed)]
public class ProductsService : IProductsService
{
  public ProductsDataSet GetDataSet()
  {
    // 非同期実行を確認するため追加(実環境では不要)
    System.Threading.Thread.Sleep(3000);

    ProductsDataSet dataset = new ProductsDataSet();
    ProductsTableAdapter adapter = new ProductsTableAdapter();
    adapter.Fill(dataset.Products);
    return dataset;
  }

  public int Update(ProductsDataSet dataset)
  {
    // 非同期実行を確認するため追加(実環境では不要)
    System.Threading.Thread.Sleep(3000);

    TableAdapterManager manager = new TableAdapterManager();
    manager.ProductsTableAdapter = new ProductsTableAdapter();
    return manager.UpdateAll(dataset);
  }
}

上記コードの ProductsDataSet, ProductsTableAdapter, TableAdapterManager は、Visual Studio のデータソース構成ウィザードを使って自動生成させた .xsd ファイル下の .Designer.cs に含まれる型付 DataSet + TableAdapter + TableAdapterManager です。

自動生成された IProductsService.cs ファイルに含まれるインターフェイス IProductsService の定義も上記に合わせて書き換えます。具体的には以下の通りです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using NorthwindProducts;

[ServiceContract]
public interface IProductsService
{
    [OperationContract]
    ProductsDataSet GetDataSet();

    [OperationContract]
    int Update(ProductsDataSet dataset);
}

(2) Windows Formsアプリケーション

上記の Web サービスを Windows Forms アプリケーションでは以下のように利用できます。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace UpdateUsingWcfService
{
  public partial class Form2 : Form
  {
    private ProductsServiceReference.
                         ProductsServiceClient client;
    private ProductsServiceReference.
                         ProductsDataSet productsDataSet;

    public Form2()
    {
      InitializeComponent();
      InitializeComponent2();
      this.client = new ProductsServiceReference.
                              ProductsServiceClient();
    }

    // 初期画面で Products テーブルのレコード一覧を表示
    private async void Form2_Load(object sender, EventArgs e)
    {
      // WCF サービスより型付 DataSet を取得

      // 非同期版メソッドを利用。戻り値は同期版と異なり:
      // Task<ProductsServiceReference.GetDataSetResponse>
      ProductsServiceReference.GetDataSetResponse response =
                           await this.client.GetDataSetAsync();

      // 目的の型付 DataSet は以下のようにして取得できる
      this.productsDataSet = response.GetDataSetResult;

      // 結果を DataGridView に表示
      this.productsBindingSource.DataSource = 
                                         this.productsDataSet;
    }

    //[データの保存]クリックでユーザーの編集結果を DB に保存
    private async void productsBindingNavigatorSaveItem_Click(
                                    object sender, EventArgs e)
    {
      this.Validate();
      this.productsBindingSource.EndEdit();

      // Web サービスに渡すのはユーザーが編集した行のみで可
      ProductsServiceReference.ProductsDataSet ds =
                 (ProductsServiceReference.ProductsDataSet)
                             this.productsDataSet.GetChanges();

      // 編集済みの型付 DataSet を Web サービスに渡して更新

      // 非同期版メソッドを利用。戻り値は同期版と異なり:
      // Task<ProductsServiceReference.UpdateResponse>
      ProductsServiceReference.UpdateResponse response = 
                             await this.client.UpdateAsync(ds);

      // 目的の戻り値(更新レコード数)は以下のようにして取得
      int n = response.UpdateResult;

      // 更新後の型付 DataSet を取得し DataGridView 書き換え

      // 非同期版メソッドを利用。戻り値は同期版と異なり:
      // Task<ProductsServiceReference.GetDataSetResponse>
      ProductsServiceReference.GetDataSetResponse response2 = 
                            await this.client.GetDataSetAsync();

      // 目的の型付 DataSet は以下のようにして取得できる
      this.productsDataSet = response2.GetDataSetResult;

      // 結果を DataGridView に表示
      this.productsBindingSource.DataSource = 
                                     this.productsDataSet;
    }

    // Form1.Designer.cs のコードにあるフォーム上の各コン
    // トロールの変数宣言をコピー。ProductsDataSet はサー
    // ビス参照の定義を使うのでその部分はコメントアウト。
    // TableAdapter と TableAdapterManager は不要(WCF サ
    // ービスを使う)なのでコメントアウト

    //private ProductsDataSet productsDataSet;
    private System.Windows.Forms.
                         BindingSource productsBindingSource;
    // ・・・以下省略・・・

    // Form1.Designer.cs のコードにある InitializeComponent()
    // メソッドのコードをコピー。メソッド名がダブらないように変
    // 更し、DataSet, TableAdapter, TableAdapterManager 関係の
    // コードをコメントアウト。Form.Text, Form.Name が "Form1"
    //  になっているので必要に応じて修正 
    private void InitializeComponent2()
    {
      this.components = new System.ComponentModel.Container();

      // BindingNavigator が使う画像が Form1.resx にしかないので、
      // ここは Form1 のままにしておく
      System.ComponentModel.ComponentResourceManager resources = 
         new System.ComponentModel.
                        ComponentResourceManager(typeof(Form1));
      // ・・・以下省略・・・

    }
  }
}

まず、Visual Studio のデータソース構成ウィザードを使って、上記 (1) WCF サービスを作った時と全く同様に、SQL Server のサンプルデータベース Northwind の Products テーブルをベースに、型付 DataSet + TableAdapter + TableAdapterManager を生成します。

生成できると「データソース」ウィンドウに Products テーブルが表示されているはずですので、それをデサイン画面で Form1 にドラッグ&ドロップすると DataGridView, BindingSource, BindingNavigatorなどのコードが自動生成され、直接 SQL Server とやり取りする Windows Forms アプリが完成するはずです。その段階で Form1 が期待通り動くことを確認してください。

確認出来たら、次に空の Form2 を追加し、それに上記 (1) WCF サービスを経由して SQL Server にアクセスして Form1 と同等の操作ができるコードを実装します。その結果が上記のコードです。具体的な手順は以下の通りです。

Visual Studio のサービス参照の追加ウィザードを利用して、上記 (1) で作成した Web サービスを参照し、サービスプロキシを自動生成させます。上記のコード例の ProductsWebServiceClient, ProductsDataSet, GetDataSetAsync, UpdateAsync 等は自動生成された Reference.cs ファイルに定義されています。

次に、Form1.Designer.cs にあるコントロールの定義と初期化のコードで必要なものを From2.cs にコピーします。上のサンプルで書いたように一式コピーして不要な部分をコメントアウトするのが間違いなくて良いと思います。

最後に、Form1.cs のイベントハンドラのコードを Form2.cs にコピーし、中身を上のサンプルのように書き換えます。

非同期版メソッド xxxAsync を利用する場合はその戻り値に注意してください。

同期版のメソッド GetDataSet、Update の場合、戻り値は (1) WCF サービスで定義した通り、それぞれ ProductsDataSet 型、int 型の値が直接返されます。

しかしながら、非同期版メソッド GetDataSetAsync、UpdateAsync の場合、戻り値はそれぞれ Task<ProductsServiceReference.GetDataSetResponse>、Task<ProductsServiceReference.UpdateResponse> となります。

即ち、Task<T> の T は ProductsDataSet 型、int 型ではなく、<operationName>Response という型のオブジェクトになります。そのクラス定義は、サービス参照を追加すると自動生成される Reference.cs というファイルに含まれています。

<operationName>Response オブジェクトから目的のオブジェクトを取得するのは、その中の public フィールド <operationName>Result にアクセスして可能です。

何故そういうことになるのかは調べ切れてません。また、ホントにその方法で良いのかと言われるとかなり不安があります。(汗) とりあえずそうすれば取得できるし非同期で動くということで上の記事は書きましたが、もう少し調べてみます。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年12月  >>
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar