連載:次世代技術につながるSilverlight入門

避けて通れない「非同期処理」を克服しよう

岩永 信之
2012/08/02
Page1 Page2 Page3

非同期処理の具体例

 それでは、Silverlightでの具体的な非同期処理の書き方について説明していこう。

 今回、サンプルとして、以下のようなSilverlightアプリを作成した。

  • WCFサービスを通して、サーバからタイトル文字列を取得して表示する
  • データ・サービスを通して、1万件のデータ(=位置情報と値を持つ単純なもの)を取得する
  • 1万件のデータの分布を画像化して表示する(Figure 7に示すようなもの)

 サンプルのソース・コードは以下の場所で公開している。

Figure 7: サンプル・アプリの表示結果
1万件のデータの分布を画像化したもの。

 データ件数が多いため、ダウンロードも画像化も数秒程度かかる。そこで、もともと非同期APIしか用意されていないネットワーク・アクセスはもちろんのこと、画像化の部分も非同期で行う必要がある。

WCFサービスの利用

 まずは、単純なWCFサービスの例を見てみよう。今回のサンプルでは、タイトル文字列をWCFサービス経由で取得して表示している。

 サーバ側では、Figure 8に示す標準の項目テンプレートを使ってWCFサービスを作成し(この例では、Silverlightアプリケーションのプロジェクトを新規作成と同時に作成したASP.NET Webアプリケーションのプロジェクトに対して、新しい項目を追加している)、List 6に示すように書き換える。文字列リテラルを返すだけのメソッドを1つだけ持っている。

Figure 8: WCFサービス作成用の項目テンプレート

[ServiceContract(Namespace = "")]
public class WcfSampleService
{
  [OperationContract]
  public string GetTitle()
  {
    return "非同期処理サンプル";
  }
}
<ServiceContract(Namespace:="")>
<AspNetCompatibilityRequirements(RequirementsMode:=AspNetCompatibilityRequirementsMode.Allowed)>
Public Class WcfSampleService
  <OperationContract()>
  Public Function GetTitle() As String
    Return "非同期処理サンプル"
  End Function
End Class
List 6: WCFサービスの実装例(上:C#、下:VB)

 Silverlightクライアント側は、Figure 9に示すように、Visual Studioの[サービス参照の追加]機能を使って、WCFサービスにアクセスするためのクラスを自動生成して使う。

Figure 9: Visual Studioの[サービス参照の追加]機能

 生成されるクラスは、EAP型の非同期APIを備えたものになっている。作成したGetTiltleメソッドを呼び出すためには、List 7に示すような書き方をする。

var sample = new AsyncSample.Web.WcfSampleServiceClient();

sample.GetTitleCompleted += (sender, e) =>
{
  this.Title.Text = e.Result;
};

sample.GetTitleAsync();
Dim sample As New AsyncSample.Web.WcfSampleServiceClient()

AddHandler sample.GetTitleCompleted,
  Sub(sender, e)
    Me.Title.Text = e.Result
  End Sub

sample.GetTitleAsync()
List 7: WCFサービスを参照する例(上:C#、下:VB)

 これが典型的なEAP型API利用のための書き方である。同期コンテキストは内部的に呼ばれていて、API利用者が直接触れる必要はない。

データ・サービスの利用

 もう1つ、WCFデータ・サービスの例を見てみよう。今回のサンプルでは、2次元的に分布するデータ列を取得して、分布図を作って表示する(このサンプルでは、VBのコードは割愛する)。

 WCFデータ・サービスは、ADO.NET Entity Frameworkなどを使ったデータ・ソースから、ODataサービスを生成してくれる機能である。Figure 10に示す標準の項目テンプレートを使ってサービスを作成する。

Figure 10: WCFデータ・サービス作成用の項目テンプレート

 今回のサンプルでは、Entity Framework Code FirstEntity Frameworkの最新版を使うには、NuGetコマンドで「Install-Package EntityFramework」と指定してインストールする)を使って、List 8に示すようなエンティティ・クラスからODataサービスを生成している。位置(X, Y)と値(Value)だけを持つ。

using System.Data.Entity;

public class MapContext : DbContext
{
  public DbSet<MapItem> MapItems { get; set; }
}

public class MapItem
{
  public int Id { get; set; }

  public double X { get; set; }
  public double Y { get; set; }
  public double Value { get; set; }
}
List 8: サンプルで使ったエンティティ・クラス(C#)

 Silverlightクライアント側では、Visual Studioの[サービス参照の追加]機能を使うこともできるが、今回はT4テンプレートを使うことにする。

 以前から、「[サービス参照の追加]機能に対して、生成されるコードのカスタマイズをしたい」という要望が多かった。その要望に応えるために、サービス参照用のT4テンプレートがNuGetパッケージ「OData Client T4」として公開されている。Figure 11に示すように、NuGetパッケージ・マネージャを使って取得できる(「リリース前のパッケージを含める」を指定する必要がある。もしくは、Silverlightアプリケーションのプロジェクトに対して、NuGetコマンドで「Install-Package Microsoft.Data.Services.Client.T4 -Pre」と指定してインストールする)。

Figure 11: NuGetパッケージ・マネージャを使ってOData Client T4パッケージを取得

 これにより、Reference.ttファイルが追加される。このファイルにより生成されたコードは、DataServiceQuery(System.Data.Services.Client名前空間)というクラスを使ってODataサービスにアクセスしている。EAPでラッピングしたクラスなどは生成されず、AMP型APIを直接利用することになる。List 9に示すような書き方をする。

this.Status.Text = "ロード開始";

var map = CreateMapService();

map.MapItems.BeginExecute(ar =>
{
  var items = (DataServiceQuery<MapItem>)ar.AsyncState;
  var result = items.EndExecute(ar);

  this.Status.Text = "画像化中";

  var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

  Task.Factory.StartNew(() => ImageRenderer.CreateBitmapBuffer(result))
    .ContinueWith(t =>
    {
      var buffer = t.Result;
      var bmp = CreateBitmap(buffer);
      this.Image.Source = bmp;
      this.Status.Text = "完了";
    }, scheduler);
},
map.MapItems);
List 9: DataServiceQueryクラスを使ってODataサービスにアクセスする例(C#)

 本節の冒頭で説明したように、今回のサンプルでは、データのダウンロード完了後、データを可視化するために画像を生成している。この処理も秒単位で時間がかかる処理で、非同期に行う必要がある。

 DataServiceQueryクラスのBeginExecute/EndExecuteメソッドは、内部的に同期コンテキストを呼び出していて、コールバックはUIスレッド上で実行されている。このため、画像生成処理をスレッド・プール上で実行するため、Taskクラスを使ってスレッド・プールへの切り替えを行っている。

 もう1点注意として、SilverlightのUI中に画像を表示するには、最終的にWriteableBitmapクラス(System.Windows.Media.Imaging名前空間)を使ってSilverlightが扱える画像形式にする必要がある。仕様上、WriteableBitmapの生成はUIスレッド中で行う必要があるため、画素データの作成だけをスレッド・プール上で行い(画素値を整数配列上で計算)、UIスレッドに切り替えてからWriteableBitmapを生成する。結果として、Figure 12に示すような動作になる。

Figure 12: List 9のコードの内部的な動作

補足: C# 5.0(とVisual Basic 11)

 非同期処理の嫌なところは、コードの見た目がずいぶん複雑化してしまうことだろう。処理したい内容自体は同じで、ただ非同期にしたいだけなのにだ。やりたいことをそのまま書けるのが理想なので、できれば、同期の場合と全く同じ書き方で非同期処理を書きたい。

 そのためにC# 5.0(とVisual Basic 11)で追加される新しい構文が、async/await(非同期メソッド/await演算子)である。

 例えば、List 2のコードをasync/awaitを使って書き直すと、List 10に示すようになる。コードは、同期版(List 1)とそっくりになる。

private async void LoadDescription()
{
  var pageUri = System.Windows.Browser.HtmlPage.Document.DocumentUri;
  var txtUri = pageUri.AbsoluteUri.Replace(pageUri.LocalPath, "/Description.txt");

  var w = new WebClient();

  var desc = await w.DownloadStringTaskAsync(txtUri);

  this.Desciption.Text = desc;
}
Private Async Sub LoadDescription()

  Dim pageUri = System.Windows.Browser.HtmlPage.Document.DocumentUri
  Dim txtUri = pageUri.AbsoluteUri.Replace(pageUri.LocalPath, "/Description.txt")

  Dim w As New WebClient()

  Dim desc = Await w.DownloadStringTaskAsync(txtUri)

  Me.Description.Text = desc

End Sub
List 10: 非同期ネットワーク・アクセスの例(List 2)をasync/awaitを使って書き直したもの(上:C#、下:VB)

 同期版との違いは以下のとおりである。

  • メソッドにasync修飾子を付ける
  • 非同期APIはTAP型のものを利用する
    • この例の場合、EAP型のDownloadStringAsyncメソッドをTAP型に変えるための拡張メソッド(DownloadStringTaskAsync)を用意した
  • 非同期処理の部分にawait演算子を付ける

 これだけで、非同期処理が実現できる。UIスレッドを止めずにI/O待ちでき、完了後には同期コンテキストを介してUIスレッドに処理が戻る。また、同期コンテキストを使うかどうかを明示的に切り替える仕組みも持っている。

 内部的な仕組みとしては、反復子(yield return)と似たような(いわば、処理の中断と再開を行う)コード生成を行って、通常の制御フローを、TaskクラスのContinueWithメソッドを使ったような非同期処理に置き換えている。

 もう一例、List 9のC#コードも書き直してみよう。List 11に示すようになる。

this.Status.Text = "ロード開始";

var map = CreateMapService();

var items = await map.MapItems.ExecuteAsync();

this.Status.Text = "画像化中";

var buffer = await TaskEx.Run(() => ImageRenderer.CreateBitmapBuffer(items));
var bmp = CreateBitmap(buffer);
this.Image.Source = bmp;

this.Status.Text = "";
List 11: ODataサービスのアクセス例(List 9)をasync/awaitを使って書き直したもの(C#)

 こちらも、APM型のBeginExecute/EndExecuteメソッドをTAP型に置き換えるための拡張メソッド(ExecuteAsync)を用意している。また、スレッド・プール上でのタスクの開始には、「Task.Factory.Start」という書き方の代わりに、「TaskEx.Run」という書き方(リリース版では「Task.Run」になる予定)に置き換わっているが、これは、次期バージョンで追加される予定の静的メソッドである。

C# 5.0(とVB 11)の利用可能次期/プレビュー版の利用方法

 C# 5.0(とVB 11)は現在、リリース候補版の状態である。現状では、以下のような手段で利用できる。

  • Visual Studio 2010
    • Visual Studio Async CTPというプレビュー・プログラムをインストールする
    • 最終的に、正式にC# 5.0(やVB 11)がサポートされるかどうかは不明(過去の経験からすると、恐らくは未サポート)
  • Visual Studio 2012 RC(リリース候補)版
    • .NET Framework 4.5(WPF 4.5やMetroスタイル・アプリ)がターゲットであれば、標準で対応
    • Silverlight 5や.NET Framework 4がターゲットであれば、Async Targeting Packという拡張をインストール

 C# 5.0(とVB 11)の正式版は、Visual Studio 2012(米国時間で8月15日に開発者向けに公開される予定)に搭載される。end of article


 INDEX
  [連載] 次世代技術につながるSilverlight入門
  避けて通れない「非同期処理」を克服しよう
    1.非同期APIの一例/非同期の仕組み
    2.非同期APIのパターン /UIスレッドへの切り替え
  3.非同期処理の具体例/補足: C# 5.0(とVisual Basic 11)

インデックス・ページヘ  「連載:次世代技術につながるSilverlight入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH