ADO.NET 2.0 における非同期コマンド実行

Pablo Castro
Microsoft Corporation

July 2004

適用対象:
   Microsoft ADO.NET 2.0

概要: ADO.NET 2.0 における新しい非同期実行機能の概要、この機能の開発目的であったシナリオ、この機能を使用する際に考慮すべき問題を紹介します。

目次

はじめに
真の非同期 I/O
新規 API 要素
アプリケーション シナリオ
考慮すべき問題
まとめ

はじめに

ADO.NET 2.0 のリリースでは、既存のシナリオを簡単に実行できるだけでなく、以前は単に不可能だったか、できても理想からかけ離れていた新しいシナリオも実現できます。

非同期のコマンド実行もその好例です。 ADO.NET の 2.0 より前のリリースでは、コマンドを実行すると、そのコマンドが完了するまでは次のコマンドを実行できませんでした。 非同期 API が加わることで、アプリケーションがデータベースの操作が完了するのを待たずに次の処理を継続する必要があるようなシナリオが実現可能となります。

ここでは、非同期データベース API の基本と、この API が有効ないくつかのシナリオを紹介します。 この API は任意のデータ アクセス プロバイダを扱うよう設計されていますが、.NET に含まれている 4 つのプロバイダのうち実際にこの API をサポートしているのは、SqlClient (SQL Server 用の .NET データ アクセス プロバイダ) のみです。 そのためここでは、これ以降サンプルや、メソッドとクラスの記述に SqlClient を使用します。 サードパーティのプロバイダ開発者もこの非同期 API を実装できるので、非同期アクセスが可能なデータベースはこれ以外にも存在する可能性があります。 これらのサンプルは、クラス名とメソッド名を適切に変更するだけで他のデータベースに使用できます。

真の非同期 I/O

.NET Framework の以前のバージョンでは、非同期のデリゲートまたは ThreadPool クラスを使用することによって、非ブロッキング実行をシミュレートすることができました。しかし、この解決方法は、単にバックグラウンドで別のスレッドをブロックするだけであり、後述のアプリケーション シナリオセクションに示すように、スレッドのブロックを避ける必要がある場合は、理想から程遠いものとなりました。

"非ブロッキング" のステートメント実行を行うデータベース アクセス API は既に存在しています。ADO です。 しかし、ADO と ADO.NET/SqlClient の実装の間には根本的な違いがあります。ADO では、バックグラウンド スレッドを生成し、データベース操作が終わるまで、呼び出し元のスレッドの代わりに該当のスレッドをブロックします。 この手法はクライアント側のアプリケーションでは機能しますが、後述の中間層およびサーバー側のシナリオでは実用的ではありません。

ADO.NET/SqlClient の非同期コマンド実行サポートは、実際に、本当の意味での非同期ネットワーク I/O (共有メモリの場合は非ブロッキングのシグナル通知) を基礎としています。 ご要望が多ければ、いずれ内部実装について文書にしたいと思います。 ここでは、"真の非同期" を行っており、特定の I/O 操作が終わるまで待機しているブロックされたバックグラウンドのスレッドは存在しない、と申し上げておきます。Windows 2000/XP/2003 オペレーティング システムのオーバーラップ I/O 機能と I/O 完了ポートの機能を利用し、単一スレッド (または少数スレッド) によって、所定のプロセスに対する未処理の要求をすべて処理することを可能にしています。

新規 API 要素

新しい ADO.NET 非同期 API は、類似の機能を持つ .NET Framework の 既存の API にならってモデリングしました。 大規模なフレームワークでは、整合性が重要となります。

非同期のメソッド

ExecuteReaderExecuteNonQueryExecuteXmlReader、および ExecuteScalar などのコマンド実行 API は、すべてADO.NET の Command オブジェクトです。 この機能のために API 上に加える変更を最小限にするため、非同期バージョンを追加するのは、他のメソッドが仕様変更する可能性のないメソッドである ExecuteReaderExecuteNonQuery、および ExecuteXmlReader のみにしました。 ExecuteScalar は、端的に言うと、ExecuteReader に、最初の行の最初の列を取得し、リーダーを閉じるという手順を加えた短い形式です。したがって、このメソッドには非同期バージョンを追加しませんでした。

.NET Framework で既に使用している非同期 API のパターンにならい、既存のそれぞれの同期メソッドに対し、処理を開始する begin パートと処理を終了する end パートの 2 つのメソッドに分かれた非同期バージョンが作成されました。 コマンド オブジェクトに追加された新しいメソッドを以下の表にまとめています。

表 1. ADO.NET 2.0 で使用可能な新しい非同期メソッド

同期メソッド非同期メソッド
"Begin" パート"End" パート
ExecuteNonQuery BeginExecuteNonQuery EndExecuteNonQuery
ExecuteReader BeginExecuteReader EndExecuteReader
ExecuteXmlReader BeginExecuteXmlReader EndExecuteXmlReader

非同期パターンはメソッドをモデリングしているので、begin メソッドがすべての入力パラメータを取得し、end メソッドが戻り値と同様に、すべての出力パラメータを提供します。 たとえば、ExecuteReader の非同期呼び出しは、以下のようになります。

IAsyncResult ar = command.BeginExecuteReader();
// ...
// 他の処理を実行します
// ...
SqlDataReader r = command.EndExecuteReader(ar);
// リーダーを使用した後、リーダーと接続を閉じます

上記のサンプルでは、BeginExecuteReader はパラメータを受け取らず (まったくパラメータを受け取らない ExecuteReader のオーバーロードにマップされます)、EndExecuteReader は、ExecuteReader と同様に、SqlDataReader を返します。

基本クラス ライブラリの他の非同期 API と組み合わせると、begin メソッドは IAsyncResult 参照を返し、これを使用して操作の状態を追跡できます。 詳細については、後出の「シグナル通知の完了」のセクションで説明します。

"async" 接続文字列キーワード

非同期コマンドを使用するためには、コマンドが実行される接続は、接続文字列を async=true と指定して初期化する必要があります。 非同期メソッドが接続文字列に async=true と指定されていない接続を使用するコマンドで呼び出されると、例外がスローされます。

所定の接続オブジェクトで同期コマンドのみを使用するとわかっている場合は、接続文字列に async キーワードを指定しないか、false に設定することをお勧めします。 非同期操作が有効になっている接続で同期操作を実行すると、リソースの利用率は著しく増大します。

同期 API と非同期 API の両方が必要な場合は、可能であれば別々の接続を使用することをお勧めします。 これが不可能であれば、async=true を指定して開かれた接続で同期メソッドを使用することもできます。この場合、通常どおりに動作しますが、パフォーマンスは若干劣化します。

シグナル通知の完了

非同期 API の基本要素に、完了のシグナル通知メカニズムがあります。 同期 API では、メソッド呼び出しは操作が終了するまで戻らないので、このような問題はありません。 非同期の場合、begin 呼び出しは直ちに戻るので、操作が実際に完了するタイミングを検出する手段が必要です。

ADO.NET は、.NET Framework の他の非同期 API と同様に、非同期コマンドの実行が終了したタイミングを検出するためのいくつかの手段を備えています。

  • コールバック: すべての begin メソッドは、パラメータとしてデリゲートおよびユーザー定義の state オブジェクトを取るオーバーロードを定義しています。 このオーバーロードが使用されると、ADO.NET は渡されたデリゲートを呼び出し、(パラメータとしてデリゲートに渡される) IAsyncResult オブジェクトを介して state オブジェクトを使用可能にします。 コールバックは、スレッド プール内のスレッドで呼び出されますが、その操作を開始したスレッドとは異なる可能性があることに注意してください。 アプリケーションによっては、適切な同期が必要な場合があります。
  • 同期オブジェクト: begin メソッドによって返される IAsyncResult オブジェクトには、イベント オブジェクトを含む WaitHandle プロパティがあります。 イベント オブジェクトは、WaitHandle.WaitAny および WaitHandle.WaitAll などの同期プリミティブで使用できます。 これによって、呼び出し側のコードは複数の保留中の操作が完了するまで待機し、操作のいずれかまたはすべてが終了した時点で通知されます。 また、クライアント コードがデータベース操作だけでなく、イベントを使用する別のアクティビティ、あるいは OS の任意の同期待機可能なハンドルを待つ必要があるシナリオも実現できます。
  • ポーリング: IAsyncResult オブジェクトには、IsCompleted ブール型プロパティもあります。 このプロパティの値は、連続して稼働する必要があるアクティビティのコードから使用できるよう、操作が完了すると true に変更されます。コードは定期的にプロパティをチェックして、値が変更されていれば結果を処理します。

3 つのいずれの場合も、操作の完了が通知された後、呼び出し側は非同期コマンドを開始した begin メソッドに対応する end メソッドを必ず呼び出す必要があります。 begin メソッドに対応する end メソッドを呼び出さないと、ADO.NET がシステム リソースのリークを起こす可能性があります。 また、end メソッドを呼び出すと、操作の結果が呼び出し側から使用できるようになります。結果とは、SqlDataReaderEndExecuteReader の場合)、影響を受けるレコード数 (EndExecuteNonQuery の場合)、または XmlReaderEndExecuteXmlReader の場合) です。

end メソッドを呼び出すときに操作の完了の待機を指定しなくても、まったく問題ありません。 この場合、メソッドは同期操作となり (機能的に言った場合。実際は非同期のままです)、操作が完了するまでブロックされます。

アプリケーション シナリオ

"目玉となる要素" を基に機能を設計するのは魅力的でも安易すぎるので、搭載する新機能を網羅するようなアプリケーション シナリオを作成することにより、それを避けるように心がけています。 非同期 API を設計する際に想定している主なシナリオをいくつか紹介します。 一見すると非同期コマンドを選択するのがふさわしく見えますが、十分に検討すると必ずしもそうとは限らない例も 1 つ含まれています。

ステートメントを並行して実行する

非同期コマンドを実行するシナリオで一般に関心を集めるのは、同じまたは異なるデータベース サーバーに対し、複数の SQL ステートメントを並行して実行するというものです。

アプリケーションの中で、特定の従業員についての情報を表示する必要があり、情報の一部は人事データベースに、給与関連の情報は経理データベースに存在するとします。 1 つ目のステートメントが完了するのを待って 2 つ目を開始するのではなく、同時に両方のデータベースにクエリを送り並行して処理できたら便利です。

以下に例を示します。

// 構成ファイルまたは同様の機能から接続文字列を
// 取得します
// 注: これらの接続文字列には "async=true" の指定が必要です
// 例: 
// "server=myserver;database=mydb;integrated security=true;async=true"
string connstrAccouting = GetConnString("accounting");
string connstrHR = GetConnString("humanresources");

// データベースごとに 1 つずつ、2 つの接続オブジェクトを定義します
using(SqlConnection connAcc = new SqlConnection(connstrAccounting))
using(SqlConnection connHumanRes  = new SqlConnection(connstrHR)) {

  // 1 番目の接続を開きます
  connAcc.Open();

  // "employee_info" ストアド プロシージャに含まれる 1 番目のクエリの
  // 実行を開始します
  SqlCommand cmdAcc = new SqlCommand("employee_info", connAcc);
  cmdAcc.CommandType = CommandType.StoredProcedure;
  cmdAcc.Parameters.AddWithValue("@empl_id", employee_id);
  IAsyncResult arAcc = cmdAcc.BeginExecuteReader();

  // この時点では、"employee_info" ストアド プロシージャはサーバー上で実行されており、
  // 同時にこのスレッドが実行されています
  
  // ここで 2 番目の接続を開きます
  connHumanRes.Open();

  // 人事のサーバーに対する 2 番目のストアド プロシージャの
  // 実行を開始します
  SqlCommand cmdHumanRes = new SqlCommand("employee_hrinfo", 
                                          connHumanRes);
  cmdHumanRes.Parameters.AddWithValue("@empl_id", employee_id);
  IAsyncResult arHumanRes = cmdHumanRes.BeginExecuteReader();

  // この時点では、両方のクエリが同時に実行中です
  // このスレッドからさらに別の処理を実行することもできます
  // あるいは、両方のコマンドが終了するまで単に待機することもできます
  // ここでは待機しています
  SqlDataReader drAcc = cmdAcc.EndExecuteReader(arAcc);
  SqlDataReader drHumanRes = cmdHumanRes.EndExecuteReader(arHumanRes);

  // ここで、結果をレンダリングできます
  // たとえば、ASP.NET Web コントロールにリーダーを連結し、
  // リーダーをスキャンして WebForms フォームで情報を描画します
}

ここでは、EndExecuteReader を 1 回呼び出すだけです。データベース操作が終了するまで、ほかに必要な処理はありません。 EndExecuteReader は操作が完了するまでブロックされ、SqlDataReader オブジェクトを返します。

複数のアクティビティの終了まで待機する必要があり、そのすべてが非同期の ADO.NET 操作とは限らないといった、さらに高度なシナリオでは、WaitHandle.WaitAll または WaitHandle.WaitAny を使用できます。また、IAsyncResult.WaitHandle は同期に使用できるイベント オブジェクトを含んでいます。

このようなさらに高度な手法を用いたアプリケーションの例として、順不同のレンダリングが挙げられます。 複数のデータ ソースを持つ ASP.NET ページがあるとします。 複数のコマンドが実行でき、他のデータベースが別の処理を行っている間に、終了したコマンドに対応するページの部分をレンダリングします。 この方法では、どの処理が最初に終了するかには関係なく、使用可能なデータがある限り処理は進行します。

ここで、Northwind データベースの使用例から興味深い部分を掲載します (この例は、ここに付属する zip ファイルに完全な形で含まれています)。 一般にこのような方法は、異なるデータベースに対して操作を行う場合、あるいはデータベース サーバーの能力が十分にあって同時にすべてのクエリを処理できる場合により有効となります。

// 注: "connstring" で示される接続文字列には "async=true"  
 // の指定が必要です。例 : 
 // "server=myserver;database=mydb;integrated security=true;async=true"

 // ここでは 3 つの接続を使用します
 using(SqlConnection c1 = new SqlConnection(connstring))
 using(SqlConnection c2 = new SqlConnection(connstring))
 using(SqlConnection c3 = new SqlConnection(connstring))
 {
  // 顧客情報を取得します
  c1.Open();
  SqlCommand cmd1 = new SqlCommand(
    "SELECT CustomerID, CompanyName, ContactName FROM Customers " +
    "WHERE CustomerID=@id", c1);
  cmd1.Parameters.Add("@id", SqlDbType.Char, 5).Value = custid;
  IAsyncResult arCustomer = cmd1.BeginExecuteReader();

  // 注文を取得します
  c2.Open();
  SqlCommand cmd2 = new SqlCommand(
    "SELECT * FROM Orders WHERE CustomerID=@id", c2);
  cmd2.Parameters.Add("@id", SqlDbType.Char, 5).Value = custid;
  IAsyncResult arOrders = cmd2.BeginExecuteReader();

  // ユーザーが注文を選んだら、注文の詳細を取得します
  IAsyncResult arDetails = null;
  SqlCommand cmd3 = null;
  if(null != orderid) {
   c3.Open();
   cmd3 = new SqlCommand(
      "SELECT * FROM [Order Details] WHERE OrderID=@id", c3);
   cmd3.Parameters.Add("@id", SqlDbType.Int).Value =         
                                                    int.Parse(orderid);
   arDetails = cmd3.BeginExecuteReader();
  }

  // WaitForMultipleObjects 用の待機ハンドルの配列を構築します
  WaitHandle[] handles = new WaitHandle[null == arDetails ? 2 : 3];
  handles[0] = arCustomer.AsyncWaitHandle;
  handles[1] = arOrders.AsyncWaitHandle;
  if(null != arDetails)
   handles[2] = arDetails.AsyncWaitHandle;

  // コマンドが完了するまで待機して、データが返されたら
  // ページ コントロールをレンダリングします
  SqlDataReader r;
  for(int results = (null==arDetails) ? 1 : 0; results < 3;results++) {

   // ハンドルが返るまで待機し、返された結果を処理します
   int index = WaitHandle.WaitAny(handles, 5000, false); // 5 秒

   if(WaitHandle.WaitTimeout == index)
    throw new Exception("Timeout");

   switch(index) {
    case 0: // 顧客クエリの準備完了
     r = cmd1.EndExecuteReader(arCustomer);
     if (!r.Read())
      continue;
     lblCustomerID.Text = r.GetString(0);
     lblCompanyName.Text = r.GetString(1);
     lblContact.Text = r.GetString(2);
     r.Close();
     break;

    case 1: // 注文クエリの準備完了
     r = cmd2.EndExecuteReader(arOrders);
     dgOrders.DataSource = r; // 注文グリッドへのデータの連結
     dgOrders.DataBind();
     r.Close();
     break;

    case 2: // 詳細クエリの準備完了
     r = cmd3.EndExecuteReader(arDetails);
     dgDetails.DataSource = r; // 詳細グリッドへのデータの連結
     dgDetails.DataBind();
     r.Close();
     break;
    }
   }
  }
 }
}

上記の 2 つの例では、従来の同期 ADO.NET および非同期デリゲート、QueueUserWorkItem などのスレッド プール API、またはユーザー作成のスレッドを使用することによって、同様の効果が実現されていることに注目してください。しかし、いずれの場合も各コマンドがスレッドをブロックしています。 クライアント側のアプリケーションなど、シナリオによってはスレッドをブロックするのは良い方法ですが、中間層およびサーバー側のアプリケーションでは、スケーラビリティを犠牲にすることになります。 これについては、次のセクションで詳しく説明します。

非ブロッキング ASP.NET ハンドラとページ

Web サーバーは一般に、さまざまなスレッド プールを使用して、Web ページ リクエスト (あるいは、Web サービスや HttpHandler 呼び出しなど、関連する他の種類の要求) を処理するスレッドを管理します。

負荷の高いデータベース ドリブンな Web サイトでは、同期データベース API を使用すると、データベース サーバーが結果を返すまで待機する間、スレッド プールのスレッドの大部分がブロックされることになります。 この場合、Web サーバーでは、CPU およびネットワークをほとんど使用していないにもかかわらず、新しい要求は受け付けず、有効なスレッドもほとんどない状態になります。

ASP.NET には、"非同期 HTTP ハンドラ" と呼ばれる構造があります。これは、IHttpAsyncHandler を実装するクラスであり、"ashx" という拡張子で ASP.NET ファイルと関連付けられています。 これらのクラスでは、非同期に要求を処理して応答を生成することができます。 この機能では、複数の非同期 ADO.NET コマンドを 1 つにまとめることができます。

IHttpAsyncHandler の詳細については、MSDN の IHttpAsyncHandler インターフェイス を参照してください。 また、その使用方法についての記事が、MSDN Magazine の Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code (英語) にあります (ここには、実際に全体がどのように動作しているかを示すわかりやすい図があります。筆者自身このような図を自分で描けたら、と思っていますが・・・)。

ここに付属のサンプルに含まれるファイル asyncorders.cs および asyncorders.ashx は、簡単で実際に動作するこの技法のサンプルです。 関連する部分を以下に掲載します。

public class AsyncOrders : IHttpAsyncHandler
{
 protected SqlCommand _cmd;
 protected HttpContext _context;
 
 // 非同期実行のサポートは、BeginProcessRequest と
 // EndProcessRequest の間に分かれて記述されています

 public IAsyncResult BeginProcessRequest(HttpContext context,                  
                                         AsyncCallback cb, 
                                         object extraData) {
  // 注文をリストする必要がある顧客の ID を取得します
  // (クエリ文字列内に記述)
  string customerId = context.Request["customerId"];
  if(null == customerId)
   throw new Exception("No customer ID specified");

  // 構成ファイルから接続文字列を取得します
  string connstring = 
                ConfigurationSettings.AppSettings["ConnectionString"];

  // データベースに接続し、クエリを起動します
  SqlConnection conn = new SqlConnection(connstring);
  try {
   conn.Open();

   // ここではストアド プロシージャを使用するが、任意のステートメントにすることが可能です
   _cmd = new SqlCommand("get_orders", conn);
   _cmd.CommandType = CommandType.StoredProcedure;
   _cmd.Parameters.AddWithValue("@ID", customerId);
   // コマンドの実行を開始します。 このメソッドは
   // クエリをデータベースにポストし、
   // 結果を待たずに戻ります
   
   // 注: ASP.NET から渡されるコールバックを BeginExecuteReader に 
   // 渡しています。ADO.NET は、最初のデータベースの結果が返ったら、
   // 直接 cb を呼び出します。 ユーザー定義のコールバックを使用して
   // 適切な ASP.NET を呼び出すことも可能です
   IAsyncResult ar = _cmd.BeginExecuteReader(cb, extraData);

   // EndProcessRequest で使用するために HttpContext を保存します
   _context = context;

   // ADO.NET の IAsyncResult を直接返します
   // 高度なアプリケーションでは、IAsyncResult の独自の実装が
   // 必要となることもあります
   return ar;
  }
  catch {
   // 問題が発生した場合のみ、接続を閉じます
   // 問題がなければ、非同期ハンドラを使用して閉じます
   conn.Close();
   throw;
  }
 }

 // ASP.NET は非同期操作が終了したことを検出すると
 // このメソッドを呼び出します
 public void EndProcessRequest(IAsyncResult result) {
  try {
   // データベースから結果を取得します
   SqlDataReader reader = _cmd.EndExecuteReader(result);

   // ページをレンダリングします
   RenderResultsTable(_context, "Orders (async mode)", reader);
  }
  finally {
   // このメソッドから戻る前に必ず接続を
   // 閉じます
   _cmd.Connection.Close();
   _cmd = null;
  }
 }

 // AsyncOrders メンバの続き
 // ...
}

上記のサンプルでは、HTTP ハンドラが入力パラメータを処理し、データベース クエリを起動し、BeginProcessRequest メソッドから戻る方法を示しています。 メソッドから戻ることによって、制御は ASP.NET に返ります。ASP.NET は、このスレッドを再利用して、データベース サーバーがクエリを処理している間に、別の要求を処理できる状態になっています。 処理が完了すると、シグナル通知メカニズムによって EndProcessRequest が呼び出され、次にページのレンダリングを完了させます。 EndProcessRequest は、同じスレッドまたは別のスレッドで呼び出されます。

ページのレンダリングに複数のクエリを必要とするシナリオでは、すべてが同じデータベースに対するクエリである場合、クエリを単一のバッチとして一緒に送信できます。あるいは複数の接続を使用し、複数の非同期コマンドの実行を開始することもできます。 後者の場合は、コマンドの完了を調整し、すべてのコマンドが終了したとき ASP.NET に通知するために追加のコードが必要となります。

WinForms アプリケーションの応答状態を維持する

長時間実行されるデータベース操作を WinForms アプリケーションから実行する場合、このような操作を実行中にアプリケーションがフリーズした経験をお持ちだと思います。 これはデータベースへの呼び出し時にイベント ハンドラがブロックされるためであり、この間イベント ハンドラは他の Windows メッセージを処理できません。

この問題を回避するために、非同期コマンド実行を使用したくなるものです。 結局必要なのは、コマンドを実行したら直ちにイベント ハンドラから戻るということです。 ただし、この場合、WinForms アプリケーションは扱いが難しくなります。WinForms コントロールは、作成されたスレッド以外のスレッドから触れることができません。これは、ユーザー プログラムから ADO.NET 非同期操作を開始できず、コールバック内でコントロールを新しいデータに更新したり、データ連結操作を実行したりできないことを意味します。 データは UI スレッドにマーシャリングして、そこで更新する必要があります。 さらに問題を複雑にするのは、多くの場合 UI を満たすためには多くのクエリを必要とし、その一部は直前の結果によって左右されるということです。 つまり、多くの非同期クエリを調整し、すべての結果を UI スレッドにマーシャリングし、その後 UI コントロールを更新する必要があります。

もっと簡単な方法は、.NET 2.0 に組み込まれる新しい BackgroundWorker クラスを使用することです。 このクラスを使用すると、UI スレッドをブロックせずに従来の同期データベース操作を実行できます。 BackgroundWorker クラスの詳細は、.NET Framework 2.0 のドキュメントがリリースされたらそちらを参照してください。

複数の選択肢を検討した上で、非同期コマンドが有効と判断できる場合に、非同期コマンドを使用してください。その場合、必要な考慮事項のすべてに十分注意してください。 機能の設計は、スレッドのブロッキングの回避が重要となるシナリオにより近づけることを目標にしました。一般に、これはクライアント側のアプリケーションではあまり重要ではないので、BackgroundWorker を単純にすることが目的にかなっています。

考慮すべき問題

ここでは、非同期コマンドを実行する際に注意が必要な考慮事項をいくつか挙げます。

エラー処理

エラーはコマンド実行中のあらゆるタイミングで発生する可能性があります。 ADO.NET は、実際のデータベース操作を開始する前にエラーを検出すると、begin メソッドから例外をスローします。これは、ExecuteReader または類似のメソッドへの呼び出しから直接例外を受け取る同期処理の場合とよく似ています。 これには、無効なパラメータ、関連オブジェクトの不良な状態 (コマンドに対する接続セットがない、など)、なんらかの接続の問題 (サーバーやネットワークのダウンなど) などがあります。

現在のところ、サーバーに処理を送信しユーザーに処理を戻した後は、問題を検出してもその時点でそれをユーザー プログラムに通知する手段がありません。 単に例外をスローすることはできません。処理を仲介しているとき、スタックには使用すべきユーザー コードがないため、例外をスローしてもユーザー プログラムからそれをキャッチできないのです。 現在行っているのは、エラー情報を別の場所に保管し、処理は完了したものとして通知することです。 後で end メソッドが呼び出されたとき、処理中にエラーがあったことを検出して例外をスローします。

要するに、begin メソッドおよび end メソッドの両方でエラーを処理する準備が必要ということです。

処理のブロッキング

非同期実行を使用する場合でも、I/O をブロックするいくつかのシナリオがあります。SqlClient に固有な、ブロッキングの可能性がある呼び出しを以下に挙げますが、すべてを網羅しているわけではありません。

  • Begin メソッド: 数多くのパラメータまたは非常に長い SQL ステートメントを送信する必要がある場合、ネットワークへの出力をブロックすることがあります。
  • SqlDataReader.Read: アプリケーションがデータを読み込む速さが、サーバーの処理能力やクライアントへのネットワークの伝送能力を上回る場合、Read() はネットワークの読み込みをブロックすることがあります。
  • SqlDataReader.Get*: データリーダーには、CLR および SQL タイプのシステムの各データ タイプに応じた Get メソッドがあります (たとえば、CLR タイプの GetString、SQL タイプの GetSqlString など)。CommandBehavior.SequentialAccessbegin 呼び出しで使用されると、これらの Get メソッドは、ネットワークの読み込みをブロックすることがあります。
  • SqlDataReader.Close() および SqlDataReader.Dispose(): これらのメソッドは、使用されていない保留中の行がある場合、または出力パラメータがネットワークからまだフェッチされていない場合に、ブロッキング状態になることがあります。

保留中のコマンドのキャンセル

コマンド オブジェクトは、実行中のコマンドをキャンセルするための Cancel() メソッドを備えています。 しかし、少なくとも ADO.NET 2.0 Beta 1 の段階には、非同期コマンドに対してこのメソッドをサポートしない予定です。 .NET Framework 2.0 の最終リリースでこれをサポートするかどうか、あいにく現時点で確実なことは申し上げられません。

バージョン

この機能で筆者が気に入っている点の 1 つは、SqlClient でサポートされるすべてのバージョンの SQL Server で動作するということです。つまり、SQL Server 7.0、SQL Server 2000、次期の SQL Server 2005 です。

ただし、1 つ注意点があります。 プレリリース版の SQL Server 2005 サーバーでは、共有メモリ上での非同期実行はサポートしていません。 したがって、サーバーとクライアントが同じコンピュータ上にある状態で非同期コマンドを使用したい場合は、サーバー名として localhost を使用するか、サーバー名に tcp: を追加して、強制的にプロバイダが共有メモリではなく TCP/IP を使用するようにしてください。

さらに、この機能は Windows NT ベースのオペレーティング システム (Windows 2000、Windows XP、Windows 2003 Server と、将来の Windows のバージョン) にのみ存在する特定の機能に大きく依存しています。 非同期実行は、Windows 9x および Windows Me ではサポートされていません。

メモ   サンプルを実行するには、.NET Framework 2.0 が必要です。 May CTP 版や Beta 1 版などのプレリリース版も使用できます。

まとめ

非同期コマンド実行は、ADO.NET の強力な機能拡張です。若干複雑性が増しますが、高いスケーラビリティを持つ新しいシナリオを実現できます。

ほかの複雑な機能と同様、非同期コマンド実行は、本当に必要な場合にのみ使用することをお勧めします。アプリケーションにコードを追加するため、将来の担当者が理解し保守する上で余分な負担がかかるからです。

この新機能が皆様のお役に立つことを望んでいます。質問、コメント、フィードバックなどがありましたら、ADO.NET ニュースグループにお送りください。 筆者も含む ADO.NET チームのメンバが数人、ほぼ毎日のようにニュースグループをチェックしています。 ニュースグループ サーバーは、msnews.microsoft.com です。また、ADO.NET のニュースグループは、microsoft.public.dotnet.framework.adonet です。