.NET でのタスク ベースの非同期パターン (TAP): 概要

.NET では、新規開発に推奨される非同期設計パターンは、タスク ベースの非同期パターンです。 それは、非同期操作を表すために使用される、System.Threading.Tasks 名前空間の Task および Task<TResult> 型に基づいています。

名前付け、パラメーター、および戻り値の型

TAP では、非同期操作の開始と終了を表すために単一のメソッドが使用されます。 これは、非同期プログラミング モデル (APM または IAsyncResult) パターンとイベントベースの非同期パターン (EAP) の両方とは対照的です。 APM では、BeginEnd メソッドが必要です。 EAP では、Async サフィックスを持つメソッドが必要であり、1 つ以上のイベント、イベント ハンドラー デリゲート型、および EventArg 派生型も必要です。 TAP の非同期メソッドには、待機可能な型を返すメソッドの操作名の後ろに Async サフィックスが含まれます (TaskTask<TResult>ValueTaskValueTask<TResult> など)。 たとえば、Task<String> を返す非同期の Get 操作を GetAsync と名付けることができます。 既に Async サフィックスの付いた EAP メソッド名を含むクラスに TAP メソッドを追加する場合は、代わりに TaskAsync サフィックスを使用します。 たとえば、既にクラスに GetAsync メソッドが含まれている場合は、GetTaskAsync という名前を使用します。 メソッドによって非同期操作が開始されるが、待機可能な型が返らない場合は、その名前を BeginStart、またはこのメソッドが操作の結果を返したりスローしたりしないことを示すその他の動詞で始める必要があります。  

対応する同期メソッドにより void または TResult 型が返されるかどうかに応じて、TAP メソッドは System.Threading.Tasks.Task または System.Threading.Tasks.Task<TResult> を返します。

TAP メソッドのパラメーターには、対応する同期メソッドと同じパラメーターを、同じ順序で指定する必要があります。 ただし、out パラメーターと ref パラメーターはこの規則に該当せず、すべて回避する必要があります。 out パラメーターまたは ref パラメーターで返されるデータは、代わりに複数の値を格納するために、タプルまたはカスタム データ構造を使用して、TResult により返される Task<TResult> の一部として返す必要があります。 また、TAP メソッドに対応する同期メソッドでは提供されていない場合でも、CancellationToken パラメーターの追加を検討してください。

タスクの作成、操作、または組み合わせのためだけに使用されるメソッド (メソッド名またはメソッドが属する型の名前でメソッドの非同期の意図が明確な場合) は、この名前付けパターンに従う必要はありません。このようなメソッドは、連結子と呼ばれることもあります。 連結子の例には、WhenAll および WhenAny があります。詳細については、記事「タスク ベースの非同期パターンの利用」の「タスク ベースの組み込み連結子の使用」セクションを参照してください。

非同期プログラミング モデル (APM) やイベント ベースの非同期パターン (EAP) など、従来の非同期プログラミング パターンで使用される構文とは異なる TAP 構文の例については、「非同期プログラミングのパターン」を参照してください。

非同期操作の開始

TAP に基づく非同期メソッドは、引数の検証や非同期操作の開始などの少量の作業を同期をとって実行してから結果のタスクを返すことができます。 このような同期作業は必要最低限にし、非同期メソッドからすぐに制御を戻すようにします。 制御をすぐに戻す理由は次のとおりです。

  • 非同期メソッドはユーザー インターフェイス (UI) スレッドから呼び出される可能性があるため、同期作業の実行に時間がかかると、アプリケーションの応答性が低下します。

  • 複数の非同期メソッドが同時に起動される可能性があります。 そのため、非同期メソッドの同期部分の作業に時間がかかると、他の非同期操作の開始が遅れ、コンカレンシーの利点が低減します。

場合によっては、操作の完了に必要な作業の量は、操作を非同期に起動するのに必要な作業量よりも少なくなります。 このようなシナリオの例にはストリームからの読み取りがあり、既にメモリ バッファーにあるデータを読み取ることで読み取り操作が完了する場合です。 このような場合は、操作を同期をとって実行し、既に完了しているタスクを返すことができます。

例外

非同期メソッドは、使用エラーに応答して非同期メソッド呼び出しからスローされる例外のみを発生する必要があります。 運用コードでは使用エラーを発生させないようにする必要があります。 たとえば、null 参照 (Visual Basic では Nothing) がメソッドの引数の 1 つとして渡されたときにエラー状態 (通常、ArgumentNullException 例外によって表される) が発生する場合、呼び出し元のコードを変更し、確実に null 参照が渡されないようにすることができます。 他のエラーの場合はすべて、非同期メソッドの実行中に発生する例外を、返されるタスクに割り当てます。これは、タスクが返される前に非同期メソッドが同期をとって行われる場合でも同じです。 通常、タスクに含まれる例外は最大でも 1 つです。 ただし、タスクが複数の操作 (WhenAll など) を表す場合は、複数の例外を単一タスクに関連付けることができます。

ターゲット環境

TAP メソッドを実装するときに、非同期実行をどこで行うかを決定できます。 スレッド プールでワークロードを実行したり、(操作の実行のほとんどでスレッドにバインドされないように) 非同期 I/O を使用して実装したり、特定のスレッド (UI スレッドなど) で実行したり、任意の数の可能なコンテキストを使用したりすることができます。 TAP メソッドは何も実行せず、システムの他の場所で発生した何らかの条件を表す Task を返すのみの場合もあります (待ち行列データ構造に届いたデータを表すタスクなど)。

TAP メソッドの呼び出し元は、結果的に生成されるタスクに同期的に応答することで、TAP メソッドの完了を待つことをブロックできます。あるいは、非同期操作の完了時に追加 (継続) コードを実行できます。 継続コードの作成者は、そのコードが実行される場所を制御できます。 継続コードは、Task クラス (ContinueWith など) のメソッドによって明示的に作成するか、継続の上位にビルドされる言語サポート (C# の await、Visual Basic の Await、F# の AwaitValue など) を使用して暗黙のうちに作成できます。

タスクの状態

Task クラスは、非同期操作の有効期間を提供し、そのサイクルは、TaskStatus 列挙型によって表されます。 Task および Task<TResult> から派生する型のコーナー ケースと、スケジューリングからの構造の分離をサポートするため、Task クラスでは Start メソッドが公開されています。 Task パブリック コンストラクターにより作成されるタスクは、ライフ サイクルがスケジュールされていない Created 状態から始まり、これらのインスタンスで Start が呼び出されるときにのみスケジュールされることから、コールド タスクと呼ばれます。

他のすべてのタスクは、ホットな状態からライフ サイクルが始まります。つまり、タスクが表す非同期操作が既に開始され、それらのタスクの状態は TaskStatus.Created 以外の列挙値であることを意味します。 TAP メソッドから返されるすべてのタスクをアクティブにする必要があります。 TAP メソッドで、返すタスクをインスタンス化するためにタスクのコンストラクターを内部使用する場合、その TAP メソッドでは、タスクを返す前に Task オブジェクトで Start を呼び出す必要があります。 TAP メソッドのコンシューマーは、返されたタスクがアクティブであるものと推定しても問題はなく、TAP メソッドから返された Start 上で Task 呼び出しを試行しないようにする必要があります。 アクティブなタスク上で Start を呼び出すと、InvalidOperationException 例外になります。

取り消し (省略可能)

TAP では、取り消しは非同期メソッドの実装側とコンシューマーのどちらでも省略可能です。 操作の取り消しを許可する場合、キャンセル トークン (CancellationToken インスタンス) を受け取る非同期メソッドのオーバーロードを公開します。 規則により、パラメーターには cancellationToken という名前が付けられます。

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

非同期操作は取り消し要求に対してこのトークンを監視します。 取り消し要求を受け取ると、その要求を受け入れて操作を取り消すことができます。 取り消し要求によって作業が途中で終了する場合、TAP メソッドは Canceled 状態で終了するタスクを返します。使用できる結果はなく、例外もスローされません。 Canceled 状態は、Faulted 状態や RanToCompletion 状態と共に、タスクの最終状態 (完了状態) と見なされます。 したがって、タスクが Canceled 状態の場合、IsCompleted プロパティは true を返します。 タスクが Canceled 状態で完了した場合、NotOnCanceled など、継続のオプションを継続から除外するよう指定されていない限り、タスクに登録された継続がスケジュールまたは実行されます。 言語機能を使用して取り消されたタスクを非同期に待機するコードは実行を継続しますが、OperationCanceledException またはその派生例外を受け取ります。 WaitWaitAll などのメソッドによってタスクでの同期をとって待機している状態をブロックされたコードも、例外を伴って実行を継続します。

キャンセル トークンが、トークンを受け取る TAP メソッドが呼び出される前に取り消しを要求していた場合、TAP メソッドは Canceled タスクを返す必要があります。 ただし、非同期操作の実行中に取り消し要求が出される場合、その非同期操作は取り消し要求を受け取る必要はありません。 取り消し要求の結果として操作が完了した場合にのみ、返されたタスクが Canceled 状態で終了します。 取り消しが要求されても、結果 (例外) が依然として生成される場合、タスクは RanToCompletion 状態または Faulted 状態で終了します。

何よりもまず、取り消す機能を公開することが望まれる非同期メソッドの場合、キャンセル トークンを受け入れないオーバーロードを用意する必要はありません。 取り消せないメソッドの場合、キャンセル トークンを受け取るオーバーロードを用意しません。これにより、ターゲット メソッドが実際に取り消し可能かどうかを呼び出し元に示すことができます。 取り消しを望まないコンシューマー コードは、CancellationToken を受け取るメソッドを呼び出し、引数値として None を指定することができます。 None は、既定の CancellationToken と機能的には同じです。

進行状況のレポート (省略可能)

一部の非同期操作では、進行状況の通知を行うことで利点が得られます。進行状況の通知は、通常、非同期操作の進行状況に関する情報でユーザー インターフェイスを更新するために使用されます。

TAP では、進行状況が、通常 IProgress<T> という名前のパラメーターとして非同期メソッドに渡される progress インターフェイスによって処理されます。 非同期メソッドの呼び出し時に進行状況インターフェイスを指定することで、不適切な使用方法により発生する競合状態 (操作の開始後に不適切に登録されたイベント ハンドラーで更新を検出できない場合) を排除できます。 さらに重要なのは、コンシューマー コードの判断に応じて、進行状況インターフェイスがさまざまな実装方法の進行状況をサポートできるようにすることです。 たとえば、コンシューマー コードが最新の進行状況の更新のみに留意する場合、すべての更新をバッファーに格納することを望む場合、各更新の操作を呼び出すことを望む場合、呼び出しを特定のスレッドにマーシャリングするかどうかの制御を望む場合が考えられます。 これらのオプションはすべて、特定のコンシューマーのニーズに合わせてカスタマイズされた、インターフェイスの異なる実装を使用して実現できます。 取り消しと同様、TAP の実装では、API が進行状況通知をサポートする場合にのみ、IProgress<T> パラメーターを指定する必要があります。

たとえば、前に説明した ReadAsync メソッドが読み取り済みのバイト数の形式で中間進行状況を報告できる場合、進行状況のコールバックは IProgress<T> インターフェイスとなることが考えられます。

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

FindFilesAsync メソッドから、特定の検索パターンに合ったすべてのファイルの一覧が返された場合、進行状況のコールバックで、完了した作業の割合の見積りと、現在の部分的な結果セットが示されることが考えられます。 この情報は、次のようにタプルを使用して提供されます。

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

または、次のように API 固有のデータ型を使用して提供されます。

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

後者の場合、特別なデータ型には通常 ProgressInfo サフィックスを付けます。

TAP の実装で、progress パラメーターを受け入れるオーバーロードが提供される場合、null の引数を許可する必要があります。この場合、進行状況は報告されません。 TAP の実装では、進行状況を Progress<T> オブジェクトに同期的に報告する必要があります。これにより、非同期メソッドで迅速に進行状況を提供できます。 また、進行状況のコンシューマーが、情報の処理に最適な方法と場所を決定できるようにします。 たとえば、進行状況のインスタンスはコールバックをマーシャリングし、キャプチャされた同期コンテキストでイベントを発生するように選択することができます。

IProgress<T> の実装

.NET には、Progress<T> を実装する IProgress<T> クラスがあります。 Progress<T> クラスは次のように宣言されます。

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Progress<T> のインスタンスは、非同期操作が進行状況の更新を報告するたびに発生する ProgressChanged イベントを公開します。 ProgressChanged イベントは、SynchronizationContext インスタンスがインスタンス化されたときにキャプチャされた Progress<T> オブジェクトで発生します。 同期コンテキストを利用できない場合は、スレッド プールをターゲットとして、既定のコンテキストが使用されます。 ハンドラーは、このイベントに登録することができます。 1 つのハンドラーは、利便性のために Progress<T> コンストラクターにも提供でき、ProgressChanged イベントのイベント ハンドラーと同様に作動します。 進行状況の更新は、イベント ハンドラーの実行中、非同期操作を遅延しないように、非同期に発生します。 別のセマンティクスを適用するため、別の IProgress<T> の実装を選択できます。

提供するオーバーロードの選択

ともに省略可能な CancellationToken パラメーターと IProgress<T> パラメーターの両方を TAP の実装に使用すると、4 つまでオーバーロードを要求することができます。

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

しかし、多くの TAP の実装では、取り消しや進行状況の機能が提供されないため、必要なメソッドは 1 つです。

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

TAP の実装で取り消しまたは進行状況の両方ではなく、いずれかを一方をサポートする場合は、実装に 2 つのオーバーロードを提供することができます。

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

TAP の実装で取り消しおよび進行状況の両方をサポートする場合は、4 種類のオーバーロードをすべて公開できます。 ただし、次の 2 種類のみ提供することもできます。

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

不足する 2 種類の中間の組み合わせを補足するために、開発者は None パラメーターに CancellationToken または既定の cancellationToken を渡したり、null パラメーターに progress を渡すことができます。

TAP メソッドを使用するたびに取り消しや進行状況をサポートすることを想定する場合、必要なパラメーターを受け入れないオーバーロードは省略できます。

複数のオーバーロードを公開して取り消しや進行状況を省略可能にする場合、取り消しや進行状況をサポートしないオーバーロードは、これらをサポートするオーバーロードに、取り消しの場合は None、進行状況の場合は null が渡された場合と同じように動作する必要があります。

Title 説明
非同期プログラミングのパターン 非同期操作を実行するための 3 種類のパターンとして、タスク ベースの非同期パターン (TAP)、非同期プログラミング モデル (APM)、およびイベント ベースの非同期パターン (EAP) を紹介します。
タスク ベースの非同期パターンの実装 タスク ベースの非同期パターン (TAP) の実装の 3 つの方法として、Visual Studio の C# および Visual Basic コンパイラを使用する方法、手動で行う方法、またはコンパイラと手動による方法を組み合わせた方法を説明します。
T:System.Threading.Tasks.Task タスクとコールバックを使用して、ブロックすることなく待機できる方法を説明します。
他の非同期パターンと型との相互運用 タスク ベースの非同期パターン (TAP) を使用して、非同期プログラミング モデル (APM) とイベント ベースの非同期パターン (EAP) を実装する方法について説明します。