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

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

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

非同期APIのパターン

 具体的なサンプル・コードを使った説明に入る前に、注意しておく点がある。歴史的経緯から、.NETとSilverlightの非同期APIの書き方には、以下の3つの異なるパターンが存在する。

  • APM(Asynchronous Programming Model): Begin/Endのメソッドのペアを使う
  • EAP(Event-based Asynchronous Pattern): 開始用のメソッドと、結果受け取り用のイベントのペアを使う
  • TAP(Task-based Asynchronous Pattern): Taskクラスのオブジェクトを返す1つのメソッドを使う

 Table 1にそれぞれの例(C#)を示す。

API定義 API利用
同期 U F(T x); Before();
var y = F(x);
After();
APM IAsyncResult BeginF(T x,
  AsyncCallback callback);
U EndF(IAsyncResult ar);
Before();
BeginF(x, ar =>
{
  var y = EndF(ar);
  After();
});
EAP class FArgs : EventArgs
{
  public U Result { get; }
}
void FAsync(T x);
event EventHandler<FArgs> FCompleted;
Before();
FCompleted += (sender, e) =>
{
  var y = e.Result;
  After();
};
FAsync(x);
TAP Task<U> FAsync(T x); Before();
FAsync(x).ContinueWith(t =>
{
  var y = t.Result;
  After();
});
TAP
(C# 5.0)
Before();
var y = await FAsync(x);
After();
Table1: .NETの非同期APIパターン(C#)

 APMは、基本形ともいえるもので、オーバーヘッドの少ないパターンである。しかし、マイクロソフトがユーザビリティ・テストを行った結果、正しく使える人が少なかったそうで、直接の利用は避けられる傾向にある。

 EAPは、.NET 2.0〜3.5のころによく使われたパターンである。イベントを使っているため、Visual Studioの補助を受けやすい半面、開始(=語尾が「Async」のメソッド呼び出し)と結果の受け取り(=Completedイベントのハンドラ登録)が逆順になり、コードが追いづらいという問題がある。

 TAPは、.NET 4でTaskクラスが導入されて以来、使われるようになったパターンで、今後はこのパターンに統一されていくことになる(.NET 4.5では、主要な非同期APIが全てTAPに置き換わっている)。

 特に、C# 5.0で導入されるawait演算子は、Taskクラスとの親和性がよく、TAPが有効である。Taskクラス自体は.NET 4/Silverlight 5で使えるので、それ以降であれば、TAPを前提に非同期APIを実装/利用するといいだろう。

UIスレッドへの切り替え

 前述のとおり、非同期処理の結果をUIに反映させるためには、別スレッドからUIスレッドに処理の流れを戻す必要がある。SilverlightやWPF、Metro(WinRT)でこの役割を担うのは、「ディスパッチャ(dispatcher: 配送係)」と呼ばれるオブジェクトだ。

 Silverlightでは、UI要素など、UIスレッドにひも付いたオブジェクトがDependencyObjectクラス(System.Windows名前空間)から派生している。DependencyObjectクラスはDispatcherというプロパティを持っていて、これを使ってUIスレッドへの切り替えを行う。

 List 3にディスパッチャの利用例を示す。DebugLogメソッドの内部でスレッドのIDをデバッグ出力するものとして、このコードをUIスレッド内で実行すると、List 4のような出力を得る。これは、Figure 6に示すような挙動をしている。

DebugLog("1");
Task.Factory.StartNew(() =>
{
  DebugLog("3");
  Dispatcher.BeginInvoke(() =>
  {
    DebugLog("5");
  });
  DebugLog("4");
});
DebugLog("2");
DebugLog("1")
Task.Factory.StartNew(
  Sub()
    DebugLog("3")
    Dispatcher.BeginInvoke(
      Sub()
        DebugLog("5")
      End Sub)
    DebugLog("4")
  End Sub)
DebugLog("2")
List 3: Dispatcherの利用例(上:C#、下:VB)

1 (thread id: 1)
2 (thread id: 1)
3 (thread id: 4)
4 (thread id: 4)
5 (thread id: 1)
List 4: List 3の実行結果の例
スレッドIDは実行するたびに変化する可能性がある。

Figure 6: List 3の内部挙動

同期コンテキスト

 このUIスレッドへの切り替えは、フレームワークによって流儀が異なっている。同じXAML系フレームワークのWPFやMetroスタイル・アプリ(WinRT)でも、DependencyObjectがDispatcherプロパティを持っているところまでは共通しているが、メソッドのシグネチャが少しずつ異なる。まして、Windowsフォームのような別系統のフレームワークの場合、全く違う書き方が必要になる。

 この差を吸収するためにあるのが同期コンテキスト(synchronization context)だ。SynchronizationContext(System.Threading名前空間)という抽象クラスの、Postメソッドを使って統一的な書き方ができる。

 例えば、List 3を、同期コンテキストを使って書き直すと、List 5に示すようになる。この書き方ならば、WPFやMetroスタイル・アプリでも動作可能である。

var context = SynchronizationContext.Current;

DebugLog("1");
Task.Factory.StartNew(() =>
{
  DebugLog("3");
  context.Post(state =>
  {
    DebugLog("5");
  }, null);
  DebugLog("4");
});
DebugLog("2");
Dim context = SynchronizationContext.Current

DebugLog("1")
Task.Factory.StartNew(
  Sub()
    DebugLog("3")
    context.Post(
      Sub(state)
        DebugLog("5")
      End Sub, Nothing)
    DebugLog("4")
  End Sub)
DebugLog("2")
List 5: 同期コンテキストの利用例(上:C#、下:VB)

同期コンテキストの隠ぺい

 ここまでの説明を見て、「アプリ開発者がここまで面倒なことをしなければならないのか」と疑問に思った方もいるかもしれない。この手の面倒ごとは、フレームワークの中に隠してしまって、アプリ開発のレベルでは気にしなくてもいい状態にすべきだろう。

 実際、WebClientクラスのDownlowdStringAsyncメソッドなど、.NET 2.0世代のEAP型の非同期APIは、内部で自動的に同期コンテキストを使っていて、アプリ開発者が意識する必要はない。

 ところが、これはこれでいくつかの問題がある。

  • WebClientクラスがそうなっているだけで、全てのEAP型APIが同期コンテキストを使っている保証はない
    • あくまで「同期コンテキストを使え」というガイドラインがあるだけで、守られる保証はない
  • 逆に、「同期コンテキストを使わない」という選択肢を取れない
    • スレッド・プール上で続きの処理をする方法がない(毎回、UIスレッドを経由することになり、性能上の問題になることがある)
    • 単体テストの際に困る(GUIアプリ中での動作と、テスト・プロジェクト中での動作が変わる)

 同期コンテキストを一切意識させない作りになっているフレームワークでは、この手の問題にはまってしまうことも少なくない。

 この結果、.NET 4世代のTaskクラスでは、タスクの実行場所を明示的に指定するためのタスク・スケジューラという仕組みが導入された。

タスク・スケジューラ

 Taskクラスを使う場合、どこでタスクを実行するかを明示的に指定できる。タスクの実行場所の管理を行うのがタスク・スケジューラで、TaskScheduler(System.Threading.Tasks名前空間)というクラスを使う。既定のタスク・スケジューラは、スレッド・プール上でタスクを実行する。

 以下のように、FromCurrentSynchronizationContext静的メソッドを使うことで、同期コンテキスト上で(=Silverlightなどの場合はUIスレッド上で)タスク実行するためのタスク・スケジューラを取得できる。

var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

 Task.Factory.StartNew(() =>
{
  // 同期コンテキスト上で実行したい処理
}, CancellationToken.None, TaskCreationOptions.None, scheduler);
Dim scheduler = TaskScheduler.FromCurrentSynchronizationContext()

Task.Factory.StartNew(
  Sub()
    ' 同期コンテキスト上で実行したい処理
  End Sub, CancellationToken.None, TaskCreationOptions.None, scheduler)
スレッド・プール上でタスクを実行するためにタスク・スケジューラを明示的に指定するコード(上:C#、下:VB)

 続いて次のページでは、Silverlightでの具体的な非同期処理の書き方について説明していく。


 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