タスク ベースの非同期パターン (TAP) は、Visual Studio で C# コンパイラと Visual Basic コンパイラを使用するか、手動で使用するか、コンパイラと手動メソッドの組み合わせを使用して、3 つの方法で実装できます。 以降のセクションでは、各方法について詳しく説明します。 TAP パターンを使用して、コンピューティング バインド非同期操作と I/O バインド非同期操作の両方を実装できます。 [ワークロード] セクションでは、各種類の操作について説明します。
TAP メソッドの生成
コンパイラの使用
.NET Framework 4.5 以降では、 async
キーワード (Visual Basic のAsync
) で属性付けされたすべてのメソッドは非同期メソッドと見なされ、C# および Visual Basic コンパイラは TAP を使用してメソッドを非同期的に実装するために必要な変換を実行します。 非同期メソッドは、 System.Threading.Tasks.Task または System.Threading.Tasks.Task<TResult> オブジェクトを返す必要があります。 後者の場合、関数の本体は TResult
を返す必要があります。コンパイラは、結果として得られるタスク オブジェクトを通じてこの結果を確実に使用できるようにします。 同様に、メソッド本体内で処理されない例外はすべて出力タスクにマーシャリングされ、結果のタスクは TaskStatus.Faulted 状態で終了します。 この規則の例外は、 OperationCanceledException (または派生型) がハンドルされない場合です。その場合、結果のタスクは TaskStatus.Canceled 状態で終了します。
TAP メソッドを手動で生成する
TAP パターンは、実装をより適切に制御するために手動で実装できます。 コンパイラは、 System.Threading.Tasks 名前空間から公開されるパブリック サーフェス領域と、 System.Runtime.CompilerServices 名前空間のサポート型に依存します。 TAP を自分で実装するには、 TaskCompletionSource<TResult> オブジェクトを作成し、非同期操作を実行し、完了したら、 SetResult、 SetException、または SetCanceled メソッド、またはこれらのメソッドのいずれかの Try
バージョンを呼び出します。 TAP メソッドを手動で実装する場合は、表される非同期操作が完了したときに結果のタスクを完了する必要があります。 例えば次が挙げられます。
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte,
offset As Integer, count As Integer,
state As Object) As Task(Of Integer)
Dim tcs As New TaskCompletionSource(Of Integer)()
stream.BeginRead(buffer, offset, count, Sub(ar)
Try
tcs.SetResult(stream.EndRead(ar))
Catch exc As Exception
tcs.SetException(exc)
End Try
End Sub, state)
Return tcs.Task
End Function
ハイブリッド アプローチ
TAP パターンを手動で実装し、実装のコア ロジックをコンパイラに委任すると便利な場合があります。 たとえば、コンパイラによって生成された非同期メソッドの外部で引数を検証し、例外が System.Threading.Tasks.Task オブジェクトを介して公開されるのではなく、メソッドの直接呼び出し元にエスケープできるようにする場合に、ハイブリッド アプローチを使用できます。
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
// code that uses await goes here
return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
If input Is Nothing Then Throw New ArgumentNullException("input")
Return MethodAsyncInternal(input)
End Function
Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
' code that uses await goes here
return value
End Function
このような委任が役立つもう 1 つのケースは、高速パス最適化を実装していて、キャッシュされたタスクを返す場合です。
作業負荷
コンピューティング バインドと I/O バインドの両方の非同期操作を TAP メソッドとして実装できます。 ただし、TAP メソッドがライブラリからパブリックに公開されている場合は、I/O バインド操作を伴うワークロードにのみ提供する必要があります (計算も含まれますが、純粋に計算することはできません)。 メソッドが純粋にコンピューティング バインドされている場合は、同期実装としてのみ公開する必要があります。 それを使用するコードでは、その同期メソッドの呼び出しをタスクにラップして、作業を別のスレッドにオフロードするか、並列処理を実現するかを選択できます。 また、メソッドが I/O バインドの場合は、非同期実装としてのみ公開する必要があります。
コンピューティング バインド タスク
System.Threading.Tasks.Task クラスは、計算負荷の高い操作を表すのに最適です。 既定では、 ThreadPool クラス内の特別なサポートを利用して効率的な実行を提供し、非同期計算を実行するタイミング、場所、方法を大幅に制御することもできます。
コンピューティング バインド タスクは、次の方法で生成できます。
.NET Framework 4.5 以降のバージョン (.NET Core および .NET 5 以降を含む) では、TaskFactory.StartNewのショートカットとして静的Task.Run メソッドを使用します。 Runを使用して、スレッド プールを対象とするコンピューティング バインド タスクを簡単に起動できます。 これは、コンピューティング バインド タスクを起動するための推奨メカニズムです。 タスクをより細かく制御する場合にのみ、
StartNew
を直接使用します。.NET Framework 4 では、非同期的に実行されるデリゲート (通常はAction<T>またはFunc<TResult>) を受け入れるTaskFactory.StartNew メソッドを使用します。 Action<T>デリゲートを指定すると、そのデリゲートの非同期実行を表すSystem.Threading.Tasks.Task オブジェクトが返されます。 Func<TResult>デリゲートを指定した場合、メソッドはSystem.Threading.Tasks.Task<TResult> オブジェクトを返します。 StartNew メソッドのオーバーロードは、キャンセル トークン (CancellationToken)、タスク作成オプション (TaskCreationOptions)、タスク スケジューラ (TaskScheduler) を受け取ります。これらはすべて、タスクのスケジュールと実行をきめ細かく制御できます。 現在のタスク スケジューラを対象とするファクトリ インスタンスは、Task クラスの静的プロパティ (Factory) として使用できます (例:
Task.Factory.StartNew(…)
)。タスクを個別に生成してスケジュールする場合は、
Task
型のコンストラクターとStart
メソッドを使用します。 パブリック メソッドは、既に開始されているタスクのみを返す必要があります。Task.ContinueWith メソッドのオーバーロードを使用します。 このメソッドは、別のタスクが完了したときにスケジュールされた新しいタスクを作成します。 一部の ContinueWith オーバーロードでは、キャンセル トークン、継続オプション、タスク スケジューラを受け取り、継続タスクのスケジュールと実行をより適切に制御できます。
TaskFactory.ContinueWhenAllメソッドとTaskFactory.ContinueWhenAnyメソッドを使用します。 これらのメソッドは、指定された一連のタスクがすべて完了したときにスケジュールされる新しいタスクを作成します。 これらのメソッドは、これらのタスクのスケジュール設定と実行を制御するためのオーバーロードも提供します。
コンピューティング バインド タスクでは、タスクの実行を開始する前に取り消し要求を受け取った場合、スケジュールされたタスクの実行を防ぐことができます。 そのため、キャンセル トークン (CancellationToken オブジェクト) を指定した場合は、そのトークンを監視する非同期コードにそのトークンを渡すことができます。 また、 StartNew
や Run
などの前述のいずれかのメソッドにトークンを提供して、 Task
ランタイムがトークンを監視できるようにすることもできます。
たとえば、イメージをレンダリングする非同期メソッドを考えてみましょう。 タスクの本文はキャンセル トークンをポーリングして、レンダリング中にキャンセル要求が到着した場合にコードが早期に終了できるようにすることができます。 さらに、レンダリングが開始される前にキャンセル要求が到着した場合は、レンダリング操作を防ぐ必要があります。
internal Task<Bitmap> RenderAsync(
ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for(int y=0; y<data.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
for(int x=0; x<data.Width; x++)
{
// render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
CancellationToken) As Task(Of Bitmap)
Return Task.Run(Function()
Dim bmp As New Bitmap(data.Width, data.Height)
For y As Integer = 0 to data.Height - 1
cancellationToken.ThrowIfCancellationRequested()
For x As Integer = 0 To data.Width - 1
' render pixel [x,y] into bmp
Next
Next
Return bmp
End Function, cancellationToken)
End Function
コンピューティング バインド タスクは、次の条件の少なくとも 1 つが当てはまる場合、 Canceled 状態で終了します。
取り消し要求は、タスクがRunning状態に遷移する前に、作成方法 (
StartNew
やRun
など) の引数として提供されるCancellationToken オブジェクトを介して到着します。OperationCanceledException例外は、このようなタスクの本文内で処理されません。その例外には、タスクに渡されたのと同じCancellationTokenが含まれており、そのトークンは取り消しが要求されたことを示します。
タスクの本文内で別の例外が処理されない場合、タスクは Faulted 状態で終了し、タスクを待機したり結果にアクセスしたりしようとすると、例外がスローされます。
I/O バインド タスク
実行全体をスレッドで直接サポートしてはならないタスクを作成するには、 TaskCompletionSource<TResult> 型を使用します。 この型は、関連付けられたTask<TResult> インスタンスを返すTask プロパティを公開します。 このタスクのライフ サイクルは、SetResult、SetException、SetCanceled、およびそのTrySet
バリアントなどのTaskCompletionSource<TResult>メソッドによって制御されます。
指定した期間が経過した後に完了するタスクを作成するとします。 たとえば、ユーザー インターフェイスでアクティビティを遅延させる場合があります。 System.Threading.Timer クラスには、指定した時間が経過した後にデリゲートを非同期的に呼び出す機能が既に用意されています。また、TaskCompletionSource<TResult>を使用すると、タイマーにTask<TResult>を配置できます。次に例を示します。
public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
TaskCompletionSource<DateTimeOffset> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(DateTimeOffset.UtcNow);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<DateTimeOffset>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
Dim timer As Timer = Nothing
timer = New Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(DateTimeOffset.UtcNow)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
この目的のために Task.Delay メソッドが用意されており、別の非同期メソッド内で使用して、たとえば、非同期ポーリング ループを実装できます。
public static async Task Poll(Uri url, CancellationToken cancellationToken,
IProgress<bool> progress)
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
progress As IProgress(Of Boolean)) As Task
Do While True
Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
Dim success As Boolean = False
Try
await DownloadStringAsync(url)
success = true
Catch
' ignore errors
End Try
progress.Report(success)
Loop
End Function
TaskCompletionSource<TResult> クラスには、非ジェネリックの対応するクラスがありません。 ただし、 Task<TResult> は Taskから派生しているため、単にタスクを返す I/O バインド メソッドに汎用 TaskCompletionSource<TResult> オブジェクトを使用できます。 これを行うには、ダミーの TResult
でソースを使用できます (Boolean は既定の選択ですが、 Task のユーザーが Task<TResult>にダウンキャストすることを心配している場合は、代わりにプライベート TResult
型を使用できます)。 たとえば、前の例の Delay
メソッドは、現在の時刻と結果のオフセット (Task<DateTimeOffset>
) を返します。 このような結果の値が不要な場合は、代わりにメソッドを次のようにコード化できます (戻り値の型の変更と引数の TrySetResultへの変更に注意してください)。
public static Task<bool> Delay(int millisecondsTimeout)
{
TaskCompletionSource<bool> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(true);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<bool>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
Dim timer As Timer = Nothing
Timer = new Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(True)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of Boolean)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
コンピューティング バインドタスクと I/O バインド タスクの混在
非同期メソッドは、コンピューティング バインド操作または I/O バインド操作だけに限定されるのではなく、2 つの組み合わせを表す場合があります。 実際には、多くの場合、複数の非同期操作が大規模な混合操作に組み合わされます。 たとえば、前の例の RenderAsync
メソッドでは、計算負荷の高い操作を実行して、入力 imageData
に基づいてイメージをレンダリングしました。 この imageData
は、非同期的にアクセスする Web サービスから取得できます。
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
cancellationToken As CancellationToken) As Task(Of Bitmap)
Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
Return Await RenderAsync(imageData, cancellationToken)
End Function
この例では、1 つのキャンセル トークンを複数の非同期操作でスレッド化する方法も示します。 詳細については、「 タスク ベースの非同期パターンを使用する」の「キャンセルの使用」セクションを参照してください。
こちらも参照ください
.NET