連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門

第1回 .NET開発における非同期処理の基礎と歴史

鈴木 孝明
2012/08/31
Page1 Page2

.NET Frameworkにおける非同期処理実装技術の歴史

 それでは、.NET Frameworkがこれまでに提供してきた非同期処理実装技術の進化の歩みについて、復習も兼ねながら見ていこう。大まかにいうとFigure 3のようになる。

Figure 3: .NET Frameworkにおける非同期処理システムの歩み

 これらの各技術を対比することで、最新技術である非同期メソッドの簡単さがより理解できるのではないかと思う。

 今回は非同期処理のサンプルとして、ボタンをクリックした際に、下記の一連の処理を行うものとする。

  1. ボタンをDisableにする
  2. 何か時間のかかる処理する
  3. ボタンをEnableにする

 いうまでもないことだが、最も基本となるUIスレッドに同期的な記述をするとList 1のようになる。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  Thread.Sleep(3000);  // 何か長い処理
  this.button.IsEnabled = true;
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Thread.Sleep(3000)  ' 何か長い処理
  Me.button.IsEnabled = True

End Sub
List 1: UIに同期的に処理を行う例(上:C#、下:VB)

 たったこれだけだが、これを非同期処理にすると、どういう実装になるのかを技術別に順番に見ていこう。きっと初めて非同期メソッドをご覧になる方はその簡単さに驚くことだろう。

Thread(スレッド)

 まず、.NET Framework 1.1時代からある最も原始的なThreadクラス(System.Threading名前空間)を用いた方法を紹介しよう。

 Threadクラスは、Figure 2で出てきた「別スレッド」そのものを表したもので、1つのインスタンスを生成すると、スレッドが1つ生成される。作成したスレッド上で行う処理をインスタンス生成時にデリゲートとして渡しておき、Startメソッドにより処理を開始する。List 2にその実装例を示す。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  var thread = new Thread(() =>
  {
    Thread.Sleep(3000);
    this.Dispatcher.BeginInvoke((Action)(() =>
    {
      // Dispatcherを利用してUIスレッドに処理を配送
      this.button.IsEnabled = true;
    }));
  });
  thread.Start();  // 別スレッドでの処理開始
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Dim thread1 As New Thread(
    Sub()
      Thread.Sleep(3000)
      Me.Dispatcher.BeginInvoke(
        Sub()
          ' Dispatcherを利用してUIスレッドに処理を配送
          Me.button.IsEnabled = True
        End Sub)
    End Sub)
  thread1.Start()  ' 別スレッドでの処理開始

End Sub
List 2: Threadクラスによる非同期処理の実装例(上:C#、下:VB)

 ここではラムダ式という形でThreadクラスのコンストラクタにデリゲートを登録しているが、.NET Framework 1.1時代は、ラムダ式はおろか、匿名メソッドすら存在しなかった。この場合、メソッドを用意し、そのデリゲートを直接セットすることになるのだが、その方法だと戻り値を受けることが困難だった。

 今では匿名メソッドやラムダ式があり、クロージャ*1によってローカル変数の受け渡しが簡単にできるので、このような問題はあまり露呈しなくなったが、当時はとても大変だった。

*1 変数を、自身が定義された静的なスコープで解決することができる機能。「進化したC# 2.0の状態管理、匿名メソッドとイテレータ」で詳しく解説されている。

 また先の例では、ボタンを有効な状態に戻す際に、Dispatcher(=WPF/SilverlightにおけるウィンドウのDispatcherプロパティで取得できるオブジェクト)のBeginInvokeメソッドを利用している。これは、.NET Frameworkに「ボタンなどのUIコンポーネントの操作は、UIスレッド上でしか行えない」という制約があるためだ。WPFやSilverlightの場合はDispatcher.BeginInvokeメソッドを、Windowsフォームの場合はControl.Invokeメソッドを利用することで、UIスレッドに処理を戻すことができる。

 また、これらを抽象化し、開発環境を問わずに統一的な記述ができるSynchronizationContextクラス(System.Threading名前空間)を利用してもよい。いずれにせよ、明示的にUIに処理を戻さなければならない煩雑さがある。

 そして何よりも問題なのは、開発者がスレッドというものを直接意識し、生成・管理・破棄しなければならないことだ。このような極めてシステム寄りな事象について思案しなければならないのは、コーディングにおいて大いなる雑念でしかない。可能ならば隠ぺいし、書きたい処理だけに集中したいものだ。

ThreadPool(スレッドプール)

 実は、スレッドの生成/破棄には思いのほか、コストがかかる。例えば、スレッドを1つ生成すると、約1Mbytesのメモリを消費するし(=空間的なコスト)、スレッドの生成/破棄のたびにプロセス内にロードされている全ての.dllファイルのDllMain関数が呼び出される(=時間的なコスト)。このようなオーバーヘッドを極小にするために考えられたのが「スレッドプール(ThreadPool)」だ。

 スレッドプールは、生成・使用したスレッドを破棄せずに使い回すリサイクル・センターのようなもので、TPL(Task Parallel Library: 並列処理ライブラリ)やI/O待ちなど、.NET Frameworkのいたるところで利用されるスレッド管理の根幹をなす重要な機能だ。このスレッドプールを表すThreadPoolクラス(System.Threading名前空間)を利用した実装例をList 3に示す。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  ThreadPool.QueueUserWorkItem(_ =>
  {
    Thread.Sleep(3000);
    this.Dispatcher.BeginInvoke((Action)(() =>
    {
      this.button.IsEnabled = true;
    }));
  }, null);
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  ThreadPool.QueueUserWorkItem(
    Sub(state)
      Thread.Sleep(3000)
      Me.Dispatcher.BeginInvoke(
        Sub()
          Me.button.IsEnabled = True
        End Sub)
    End Sub, Nothing)

End Sub
List 3: ThreadPoolクラスによる非同期処理の実装例(上:C#、下:VB)

 ThreadPool.QueueUserWorkItemメソッドを利用している点を除けば、Threadクラスとほとんど変わらないことが分かるだろう。パフォーマンスの観点から、明示的にスレッドを作らなければならないケースを除いて、このスレッドプールを利用する方が望ましい。

Asynchronous Programming Model(非同期プログラミング・モデル)

 .NET Frameworkで初めて提供された、スレッドを直接意識しない非同期処理システムが「APM(Asynchronous Programming Model)」だ。BeginXxxとEndXxxのメソッドのペアで実装を行う。前述のThreadやThreadPoolと大きく異なるのは、非同期処理の結果を戻り値の形で受けることができる点だ。List 4に実装例を示す。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  var method = new Func<double>(() =>
  {
    Thread.Sleep(3000);
    return Math.PI;  // 結果を返せる
  });
  method.BeginInvoke(ar =>
  {
    var result = method.EndInvoke(ar);  // 結果 : π
    this.Dispatcher.BeginInvoke((Action)(() =>
    {
      this.button.IsEnabled = true;
    }));
  }, null);
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Dim method As New Func(Of Double)(
      Function()
        Thread.Sleep(3000)
        Return Math.PI  ' 結果を返せる
      End Function)
  method.BeginInvoke(
    Sub(ar)
      Dim result = method.EndInvoke(ar)  ' 結果 : π
      Me.Dispatcher.BeginInvoke(
        Sub()
          Me.button.IsEnabled = True
        End Sub)
    End Sub, Nothing)

End Sub
List 4: APMによる非同期処理の実装例(上:C#、下:VB)

 すでにお気付きかもしれないが、記述の仕方が非常に煩雑だ。.NET Framework標準で提供されているメソッドを利用するだけならまだしも、BeginXxx/EndXxxメソッドの独自実装をサラッとはなかなか書けない。今では、その複雑さからこの手法はあまり利用されない傾向にある。

Event-based Asynchronous Pattern(イベントベースの非同期パターン)

 WindowsフォームやWebフォームが全盛期の時代に登場した、完了や進捗(しんちょく)の通知などをイベントによるコールバックの形で提供する非同期処理システムが「EAP(Event-based Asynchronous Pattern)」だ。List 5にその実装例を示す。

private readonly BackgroundWorker worker = new BackgroundWorker();

public MainWindow()
{
  this.InitializeComponent();

  // 先に非同期処理の本体や完了時処理を登録しておく
  this.worker.DoWork             += this.OnDoWork;
  this.worker.RunWorkerCompleted += this.OnCompleted;
}

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  this.worker.RunWorkerAsync();  // 非同期処理開始
}

// 非同期処理本体
private void OnDoWork(object sender, DoWorkEventArgs e)
{
  Thread.Sleep(3000);
  e.Result = Math.PI;  // 結果を返せる
}

// UIスレッド上で動作する完了時コールバック
private void OnCompleted(object sender, RunWorkerCompletedEventArgs e)
{
  var result = (double)e.Result; // 結果 : π
  this.button.IsEnabled = true;
}
Private ReadOnly worker As New BackgroundWorker()

Public Sub New()

  InitializeComponent()

  ' 先に非同期処理の本体や完了時処理を登録しておく
  AddHandler Me.worker.DoWork, AddressOf Me.OnDoWork
  AddHandler Me.worker.RunWorkerCompleted, AddressOf Me.OnCompleted

End Sub

Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Me.worker.RunWorkerAsync()  ' 非同期処理開始

End Sub

' 非同期処理本体
Private Sub OnDoWork(senser As Object, e As DoWorkEventArgs)

  Thread.Sleep(3000)
  e.Result = Math.PI  ' 結果を返せる

End Sub

' UIスレッド上で動作する完了時コールバック
Private Sub OnCompleted(senser As Object, e As RunWorkerCompletedEventArgs)

  Dim result = CType(e.Result, Double) ' 結果 : π
  Me.button.IsEnabled = True

End Sub
List 5: EAPによる非同期処理の実装例(上:C#、下:VB)

 メソッド単位で処理を意味的に分離できるため、比較的見通しよく記述できる。また、これまでに紹介した非同期処理システムでは、UIコンポーネントの操作のためにUIスレッドに戻す記述を明示的に行っていたが、EAPではUIスレッド上に戻したうえで完了処理をコールバックしてくれる*2。半面、非同期処理の開始より先に非同期処理の本体や完了処理を登録しなければならず、コードの順序が処理フローと逆転するというデメリットもある。

*2 .NET Frameworkが提供するEAPに限ってであり、独自実装時は開発者自身がUIスレッドに戻す挙動を担保しなければならない。

Task-based Asynchronous Pattern(タスクベースの非同期パターン)

 .NET Framework 4で搭載された並列処理ライブラリ「TPL」の要であり、本連載の主題である非同期メソッドを支える根幹機能でもあるTaskクラス(System.Threading.Tasks名前空間)を利用した手法が「TAP(Task-based Asynchronous Pattern)」だ。

 Taskクラスは「何か非同期的に実行する操作」を表したものと考えればよい。そこから結果を受け取ったり、完了を待機したり、完了後に続けて別の処理を行ったりできるなど、非常に高い柔軟性を持っている。List 6にそのTaskクラスを使った非同期処理の実装例を示す。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  Task.Factory.StartNew(() => Thread.Sleep(3000))
  .ContinueWith(_ =>
  {
    this.button.IsEnabled = true;
  }, TaskScheduler.FromCurrentSynchronizationContext());
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Task.Factory.StartNew(
    Sub()
      Thread.Sleep(3000)
    End Sub).ContinueWith(
    Sub(state)
      Me.button.IsEnabled = True
    End Sub, TaskScheduler.FromCurrentSynchronizationContext())

End Sub
List 6: TAPによる非同期処理の実装例(上:C#、下:VB)

 Task.Factory.StartNewメソッドは、Taskの生成と実行を一挙に行うものだ。ここで指定したデリゲートを非同期に実行するという点は最初に紹介したThreadとそっくりだが、都度、スレッドを1つ生成しているわけではない。内部ではThreadPoolが利用されており、空いているスレッドに自動的に作業が割り振られる。そのため、実行コストは最小限に抑えられている。

 ContinueWithメソッドに渡したデリゲートは、元のタスクが完了した後に実行される。また、第2引数にはTaskScheduler.FromCurrentSynchronizationContextメソッドで取得したUIスレッドに同期的なスケジューラを設定している。そのため、ContinueWithメソッドはUIスレッド上で動作することになり、ボタンの有効化を実行することができる。

 このようにTAPでは、「○○を非同期に実行してください。○○が完了したら△△の処理を実行してください。ただし、△△はUIスレッド上で動作させてください」のように処理のフローや意図を分かりやすく表現できる。

Reactive Extensions(Rx)

 Rx(Reactive Extensions)は通常のLINQをイベント処理や非同期処理に拡張したもので、.NET Framework 3.5から利用することができる準標準ライブラリ*3である。これを利用すると、List 7の例(C#のみ。VBは割愛)のようにイベントの関連付けからスレッド間の移動までを全てメソッド・チェーンの形で記述できる。

*3 .NET Framework本体には搭載されていないが、マイクロソフトが正規のプロダクトとして提供している追加のライブラリ。

public MainWindow()
{
  this.InitializeComponent();

  Observable
  .FromEventPattern(this.button, "Click") // ボタンのクリック・イベントが起きたら
  .Do(_ => this.button.IsEnabled = false) // ボタンを無効化する
  .ObserveOn(Scheduler.ThreadPool)        // その後、別スレッドに移動し
  .Do(_ => Thread.Sleep(3000))            // 何か時間のかかる処理をする
  .ObserveOn(SynchronizationContext.Current)      // 完了したらUIスレッドに処理を戻し
  .Subscribe(_ => this.button.IsEnabled = true);  // ボタンを再度有効化する
}
List 7: Rxによる非同期処理の実装例(C#)

 ここまで見てきた非同期処理とはだいぶ毛色が違うが、宣言的な記述になっているため処理フローが非常に分かりやすいのが特徴だ。また、例外処理やリトライなどにも柔軟に対応することもでき、非同期メソッドに並んで最も有用な機能の1つとなっている。

 Rxは非常に奥が深いライブラリなので、興味のある方は「連載:Reactive Extensions(Rx)入門」や「xin9le note - Rx入門」などを参考にしてほしい。

async/await

 そして最後が、.NET Framework 4.5に対応したC# 5.0やVB 11.0で搭載された「非同期メソッド」(=async修飾子とawait演算子を利用したメソッド)だ。さっそく、そのコードを見てみよう。

private async void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  await Task.Run(() => Thread.Sleep(3000));
  this.button.IsEnabled = true;
}
Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Await Task.Run(Sub()
                     Thread.Sleep(3000)
                 End Sub)
  Me.button.IsEnabled = True

End Sub
List 8: async/awaitによる非同期処理の実装例(上:C#、下:VB)

 いかがだろうか。ほぼ最初に紹介した同期処理の記述と同じであることが分かるだろう。相違点は以下の3点だけだ。

  • メソッドにasyncキーワードが付加されている
  • 実処理をTaskで記述している
  • Taskの前にawaitキーワードが付加されている

 同期処理にたったこれだけの変更を加えるだけで非同期化される。これまではボタンの再有効化のために明示的にUIスレッドに処理を戻す記述をしていたが、もはやそれすらも必要ない。また、コードがあちこちに分散することもないので処理のフローが明快になる。同期処理と同じ流れで非同期処理を書く、それが非同期メソッドの最大の魅力だ。

まとめ

 ここまで順を追って歴代の非同期処理システムについて見てきた。この流れで非同期メソッドを見ると、非同期処理を行ううえでの無駄がきれいに省かれていることが分かる。APMやEAPの記述と比べると、まるで魔法のようにさえ感じることだろう。しかし、非同期メソッドは記述が楽になるだけで、非同期処理自体が持つ本来の難しさ*4がなくなるわけではない。その点は誤解がないようにしなければならないし、十分注意してほしい。

*4 排他制御やデッドロックなど。

 さて次回は、非同期メソッドの書き方や利用上の注意点などをより詳細に解説する予定だ。ぜひ、楽しみに待っていてほしい。それでは、また次回。end of article


 INDEX
  連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門
  第1回 .NET開発における非同期処理の基礎と歴史
    1.非同期処理の必要性/非同期処理の基本のキ
  2..NET Frameworkにおける非同期処理実装技術の歴史

インデックス・ページヘ  「連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門」

@IT Special

- PR -

TechTargetジャパン

Insider.NET フォーラム 新着記事
  • Pythonの例外をサクサク理解しよう (2017/1/13)
     Pythonで例外を取り扱うための機構であるtry文やraise文などについて、サンプル多めで見ていこう
  • 構文:キャッチした例外をリスローするには? (2017/1/11)
     例外の処理時には何らかの理由で、キャッチした例外をリスローしなければならないときがある。C#やVBでこれを適切に行う方法を解説する
  • ジャグ配列 (2017/1/10)
     ジャグ配列とは「配列の配列」を表す用語。ジャグ配列では配列の特定の次元の各要素が参照する配列の要素数が異なる場合がある
  • Xamarin.Forms:ScrollViewで画面をスクロール (2016/12/28)
     ScrollViewコントロールを使うことで、画面サイズには収まりきらないUI要素をスクロールして表示する方法を解説する
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

イベントカレンダー

PickUpイベント

- PR -

アクセスランキング

もっと見る

ホワイトペーパーTechTargetジャパン

注目のテーマ

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

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