第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装:連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回)(2/2 ページ)
「魔法」のような非同期メソッドはどんな仕組みで動いているのか? その内部実装に踏み込み、「魔法」の実体を解き明かす。
コンパイラ要件
ここまでで、コンパイラが非同期メソッドをうまく別の形に変換・展開していることが確認できた。しかし、コンパイラも一定の規約に従ってこれらを実現していることは想像に難くない。ここでは、コンパイラがどのような規約でもって非同期メソッドを解釈しているのか、その要件について確認してみよう。
●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)。
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!!
Copyright© Digital Advantage Corp. All Rights Reserved.