検索
連載

第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回)(2/2 ページ)

「魔法」のような非同期メソッドはどんな仕組みで動いているのか? その内部実装に踏み込み、「魔法」の実体を解き明かす。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
前のページへ |       

コンパイラ要件

 ここまでで、コンパイラが非同期メソッドをうまく別の形に変換・展開していることが確認できた。しかし、コンパイラも一定の規約に従ってこれらを実現していることは想像に難くない。ここでは、コンパイラがどのような規約でもって非同期メソッドを解釈しているのか、その要件について確認してみよう。

Awaitableになるための条件

 List 3にもあるように、await演算子に渡すことができるインスタンスは以下の条件を満たす必要がある。

  • Awaiter(後述)を返すGetAwaiterメソッドを持つ

 これはList 5、List 6に示すようにインスタンス・メソッドでも拡張メソッドでもどちらでも構わない。メソッド名が同じであればよく、ダック・タイピング的な制約となっている。

public class Awaitable
{
  public Awaiter GetAwaiter()
  {
    return new Awaiter(this);
  }
}


Public Class Awaitable

  Public Function GetAwaiter() As Awaiter
    Return New Awaiter(Me)
  End Function

End Class


List 5: インスタンス・メソッドの例(上:C#、下:VB)


public class Awaitable
{}

public static class AwaitableEx
{
  public static Awaiter GetAwaiter(this Awaitable awaitable)
  {
    return new Awaiter(awaitable);
  }
}


Public Class Awaitable
End Class

Public Module AwaitableEx

  <Extension()>
  Public Function GetAwaiter(awaitable As Awaitable) As Awaiter
    Return New Awaiter(awaitable)
  End Function

End Module


List 6: 拡張メソッドの例(上:C#、下:VB)


Awaiterになるための条件

 一方、GetAwaiterメソッドから返されるAwaiterなインスタンスは、以下の3つの条件を満たしている必要がある。List 7はその実装イメージだ。

  • IsCompletedプロパティを持つ
  • INotifyCompletionインターフェイスを実装する
  • GetResultメソッドを持つ

public class Awaiter<T> : INotifyCompletion
{
  public bool IsCompleted{ get; }
  public void OnCompleted(Action continuation){}
  public T GetResult(){ return default(T); }
}


Public Class Awaiter(Of T)
  Implements INotifyCompletion

  Public ReadOnly Property IsCompleted() As Boolean
    Get
      Return Nothing
    End Get
  End Property
  Public Sub OnCompleted(continuation As Action) Implements INotifyCompletion.OnCompleted
  End Sub
  Public Function GetResult() As T
    Return CType(Nothing, T)
  End Function

End Class


List 7: Awaiterの実装イメージ(上:C#、下:VB)


 IsCompletedプロパティは、呼び出し時点で対象となる非同期処理が完了しているかどうかをbool値で返す。List 3にもあるように、これがfalseを返す場合、OnCompletedメソッドでの継続処理の登録に遷移する。

 INotifyCompletionインターフェイスは、OnCompletedメソッドの実装を制約するものだ。第1引数には、非同期処理の完了時にコールバックされるべき処理がデリゲートとして渡される。大事なのは、OnCompletedメソッド自体が非同期処理の完了時に呼び出されるわけではないということだ。メソッド名がOnCompletedであるために混乱しがちだが、OnCompletedメソッドの仕事は、渡されたデリゲートを対象となる非同期処理の完了時にコールバックされるように登録することだ。

 GetResultメソッドは非同期処理の結果を取得するために利用される。興味深いのは、メソッドの戻り値の型が問われないことだ。つまり、void型を戻り値とすることも可能で、これは結果として戻す値がない場合に指定される。もちろん、結果を戻す必要がある場合はその型を指定すればよい。

Awaitableパターンの独自実装

 前述の実装方法は「Awaitableパターン」と呼ばれている。.NET Framework標準ではTask型およびTask<T>型(いずれもSystem.Threading.Tasks名前空間)のみがawait演算子に対応しているが、それとは別に、開発者が独自実装を行えるだけの汎用性が残されている。ここでは、その独自実装の例を見ていくことにしよう。

Awaitable型の実装

 まずawait演算子に渡すAwaitable型を作成する。ここでは、外部からデリゲートとして与えられたタスクの非同期実行や、そのタスクが完了しているかどうかの取得、完了時にコールバックしてほしい処理の登録などを実装する。List 8にそのサンプルを示す。

class Awaitable<T>
{
  public T Result{ get; private set; }
  public bool IsCompleted{ get; private set; }
  private Action OnCompleted{ get; set; }

  private Awaitable() { }

  private void DoWorkAsync(Func<T> action)
  {
    // 今回はThreadPoolを用いて非同期処理を実行することにする
    ThreadPool.QueueUserWorkItem(_ =>
    {
      this.Result = action();
      this.IsCompleted = true;
      if (this.OnCompleted != null)
        this.OnCompleted();
    });
  }

  // 完了時に呼び出される処理の登録
  public void ContinueWith(Action action)
  {
    this.OnCompleted = action;
    if (this.IsCompleted && this.OnCompleted != null)
      this.OnCompleted(); // すでに完了している場合は即時呼び出し
  }

  // Awaiterの取得
  public Awaiter<T> GetAwaiter()
  {
    return new Awaiter<T>(this);
  }

  // Awaitable型による非同期処理の実行
  public static Awaitable<T> Run(Func<T> action)
  {
    var awaitable = new Awaitable<T>();
    awaitable.DoWorkAsync(action);
    return awaitable;
  }
}


Class Awaitable(Of T)

  Private _result As T
  Public Property Result() As T
    Get
      Return _result
    End Get
    Private Set(ByVal value As T)
      _result = value
    End Set
  End Property

  Private _isCompleted As Boolean
  Public Property IsCompleted() As Boolean
    Get
      Return _isCompleted
    End Get
    Private Set(ByVal value As Boolean)
      _isCompleted = value
    End Set
  End Property
  Public Property OnCompleted() As Action

  Private Sub New()
  End Sub

  Private Sub DoWorkAsync(action As Func(Of T))
    ' 今回はThreadPoolを用いて非同期処理を実行することにする
    ThreadPool.QueueUserWorkItem(
      Sub()
        Me.Result = action
        Me.IsCompleted = True
        If Me.OnCompleted <> Nothing Then
          Me.OnCompleted.Invoke()
        End If
      End Sub)
  End Sub

  ' 完了時に呼び出される処理の登録
  Public Sub ContinueWith(action As Action)
    Me.OnCompleted = action
    If Me.IsCompleted AndAlso Me.OnCompleted <> Nothing Then
      Me.OnCompleted().Invoke() ' すでに完了している場合は即時呼び出し
    End If
  End Sub

  ' Awaiterの取得
  Public Function GetAwaiter() As Awaiter(Of T)
    Return New Awaiter(Of T)(Me)
  End Function

  ' Awaitable型による非同期処理の実行
  Public Shared Function Run(action As Func(Of T)) As Awaitable(Of T)
    Dim awaitable = New Awaitable(Of T)()
    awaitable.DoWorkAsync(action)
    Return awaitable
  End Function

End Class


List 8: Awaitable型の実装例(上:C#、下:VB)


 ここでは簡単のために省略しているが、実際にはAwaitable型全体がスレッドセーフになるように排他制御を組み込む必要がある。

Awaiter型の実装

 次に、Awaitable型に対応するAwaiter型の実装を行う。List 9にその実装例を示す。

class Awaiter<T> : INotifyCompletion
{
  private readonly Awaitable<T> target = null;

  public Awaiter(Awaitable<T> target)
  {
    // 対応するAwaitable型を保持
    this.target = target;
  }

  public bool IsCompleted
  {
    get{ return this.target.IsCompleted; }
  }

  public void OnCompleted(Action continuation)
  {
    // 実行スレッドの同期コンテキスト上で継続処理を実行
    var context = SynchronizationContext.Current;
    this.target.ContinueWith(() =>
    {
      context.Post(_ => continuation(), null);
    });
  }

  public T GetResult()
  {
    return this.target.Result;
  }
}


Class Awaitare(Of T)
  Implements INotifyCompletion

  Private ReadOnly target As Awaitable(Of T) = Nothing

  Public Sub New(target As Awaitable(Of T))
    ' 対応するAwaitable型を保持
    Me.target = target
  End Sub

  Public ReadOnly Property IsCompleted() As Boolean
    Get
      Return Me.target.IsCompleted
    End Get
  End Property

  Public Sub OnCompleted(continuation As Action) Implements INotifyCompletion.OnCompleted
    ' 実行スレッドの同期コンテキスト上で継続処理を実行
    Dim context = SynchronizationContext.Current
    Me.target.ContinueWith(
      Sub()
        context.Post(Sub() continuation(), Nothing)
      End Sub)
  End Sub

  Public Function GetResult() As T
    Return Me.target.Result
  End Function

End Class


List 9: Awaiter型の実装例(上:C#、下:VB)


 注目すべきはOnCompletedメソッドで、指定された継続処理が呼び出し元スレッド上で動作するよう、実行スレッドの同期コンテキスト上で実行している。そのほかのIsCompletedプロパティとGetResultメソッドは、Awaitable型の状態をそのまま透過的に伝達しているだけなので、特に難しいことはないだろう。

利用例

 準備が整ったので、これらを利用して簡単な実装をしてみよう。List 10にその利用例を示す。これまで何度も利用してきたTask.Runメソッドを、Awaitable.Runメソッドに置き換えただけの簡単なものだ。

private async void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  this.display.Text = string.Empty;
  var result = await Awaitable<DateTime>.Run(() =>
  {
    Thread.Sleep(3000);
    return DateTime.Now;
  });
  this.display.Text = result.ToString();
  this.button.IsEnabled = true;
}


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

  Me.button.IsEnabled = False
  Me.display.Text = String.Empty
  Dim result = Await Awaitable(Of DateTime).Run(
    Function()
      Thread.Sleep(3000)
      Return DateTime.Now
    End Function)
  Me.display.Text = result.ToString()
  Me.button.IsEnabled = True

End Sub


List 10: Awaitable型の利用例(上:C#、下:VB)


.NET Framework 4.5以外をターゲットにした「await」

 async/awaitはC# 5.0/VB(Visual Basic)11.0で追加された機能だ。これらのキーワードを使ったコードのコンパイルを通すには、Visual Studio 2012を使用し、.NET Framework 4.5をターゲットとする必要があった。これまでの連載中でもこの要件は暗黙の前提条件となっていた。しかし、実際は.NET Framework 4.5をターゲットにしなくてもasync/awaitのキーワードを使うことができる。

 先に解説したとおり、async/await構文は.NET Framework 4.5で追加されたいくつかのクラス・ライブラリを利用した糖衣構文である。また、Visual Studio 2012に搭載されているC#/VBのコンパイラはasync/awaitキーワードを解釈可能なものであり、このコンパイラは、例えば.NET Framework 4をターゲットとした場合も利用される。つまり、Visual Studio 2012を利用し、かつasync/await構文のコンパイルを通せるだけのクラス・ライブラリ群を追加しさえすれば、.NET Framework 4.5をターゲットにしていなくても非同期メソッドを利用できるということだ。

Microsoft.Bcl.Async

 2012年10月23日、.NET Framework 4.5以外の環境下で非同期メソッドを利用するための追加ライブラリ「Microsoft.Bcl.Async」がリリースされた。これまでも「Async Targeting Pack for Visual Studio 2012」が公開されていたが、Microsoft.Bcl.Asyncライブラリはその後継となるものだ。このライブラリは、NuGetで[リリース前のパッケージを含める]を選択して「Microsoft.Bcl.Async」と検索することで取得できる(Figure 2)。


Figure 2: NuGetでの検索結果

 Microsoft.Bcl.Asyncライブラリがサポートしているプラットフォームは、Async Targeting Pack for Visual Studio 2012がサポートしていたものに赤字の項目を追加したものだ。

  • .NET Framework 4
  • Silverlight 5
  • Silverlight 4
  • Windows Phone 7.5
  • Portable Class Library

 また、Async Targeting Pack for Visual Studio 2012はWindows XPやWindows Vista上での動作を保障していなかったが、Microsoft.Bcl.Asyncライブラリにはその制約はないようだ。非同期メソッドの適応可能範囲が広がったのは非常にありがたいことだ。執筆時点ではまだプレビュー版としての位置付けではあるが、ぜひ率先して利用してみてほしい。

まとめ

 いかがだっただろうか。今回は非同期メソッドの内部に踏み込み、その動作原理をひもといてきた。連載1回目には「魔法」とさえ思えたものも、今では「よくできている」という感心に変わったのではないだろうか。また今回はコンパイラ要件にも言及し、独自実装の方法も併せて紹介した。Taskクラスが非常に汎用的で強力なため、どれほど機会があるかは分からないが、非同期メソッドを単に利用するだけでなく、必要に応じてカスタマイズすることさえもできるようなっていると思う。

 ここまで全3回にわたって、非同期メソッドの構文や使い方、注意点、さらにはカスタマイズ方法などについて解説してきた。第1回の冒頭でも述べたが、今後はますます非同期処理が当たり前になり、開発者にとってはさらにスキルが要求される厳しい時代になるだろう。今回の連載が、開発者の皆さんのスキルアップの一助になれば幸いである。Enjoy Asynchronous Programming!!

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

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

Copyright© Digital Advantage Corp. All Rights Reserved.

前のページへ |       
ページトップに戻る