タスクの並列化 (タスク並列ライブラリ)
更新 : 2011 年 3 月
タスク並列ライブラリ (TPL: Task Parallel Library) は、その名前が示すように、タスクの概念に基づいています。 タスクの並列化とは、1 つ以上の独立したタスクを同時に実行することです。 タスクは非同期操作を表しており、いくつかの点で新しいスレッドまたは ThreadPool 作業項目の作成と似ていますが、それは上位レベルの抽象化における場合です。 タスクが提供する主な利点は次の 2 つです。
システム リソースをより効率的かつスケーラブルに利用する。
背後では、タスクは ThreadPool へのキューとして配置されます。ThreadPool は山登り法に似たアルゴリズムを使用して強化されており、スループットを最大化するためにスレッド数を判別および調整します。 これによりタスクが比較的軽量化されるため、多数のタスクを作成して粒度の高い並列化を実現できます。 これを補完するため、周知のワーク スティーリング アルゴリズムによって、負荷分散が行われます。
スレッドまたは作業項目より、プログラムによる制御を詳細に行うことができる。
タスクおよびタスクを中心に構築されたフレームワークでは、待機、キャンセル、継続、信頼性の高い例外処理、詳細なステータス、カスタムのスケジュール設定などをサポートする豊富な API が用意されています。
この 2 つの理由により、.NET Framework 4 では、マルチスレッド、非同期および並列コードを記述するために、タスクで API を使用することをお勧めしています。
暗黙的なタスクの作成と実行
Parallel.Invoke メソッドには、任意の数のステートメントを同時に実行する便利な方法が用意されています。 作業項目ごとに Action デリゲートに渡すだけです。 これらのデリゲートを最も簡単に作成するには、ラムダ式を使用します。 ラムダ式では、名前付きメソッドを呼び出したり、コード インラインを指定したりできます。 次の例では、2 つのタスクを同時に作成および開始する基本の Invoke 呼び出しを示しています。
![]() |
---|
ここでは、ラムダ式を使用して TPL でデリゲートを定義します。C# または Visual Basic のラムダ式についての情報が必要な場合は、「PLINQ および TPL のラムダ式」を参照してください。 |
Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
![]() |
---|
Invoke によって背後で作成される Task インスタンスの数は、指定するデリゲートの数と等しくなくてもかまいません。TPL では、特に多数のデリゲートによるさまざまな最適化方法を採用しています。 |
詳細については、「方法: Parallel.Invoke を使用して並列操作を実行する」を参照してください。
タスクの実行をさらに制御する場合、またはタスクから値を返す場合、Task オブジェクトをより明示的に操作する必要があります。
明示的なタスクの作成と実行
タスクは、System.Threading.Tasks.Task クラスによって表されます。 値を返すタスクは、Task から継承された System.Threading.Tasks.Task<TResult> クラスで表されます。 タスク オブジェクトはインフラストラクチャの詳細を処理し、タスクの有効期間内に呼び出し元のスレッドからアクセスできるメソッドとプロパティを提供します。 たとえば、タスクの Status プロパティに任意のタイミングでアクセスして、タスクが開始されたか、完了まで実行されたか、取り消されたか、または例外がスローされたかどうかを確認できます。 状態は、TaskStatus 列挙型によって表されます。
タスクを作成するときは、タスクが実行するコードをカプセル化するユーザー デリゲートを指定します。 このデリゲートは名前付きデリゲート、匿名メソッド、またはラムダ式として表すことができます。 ラムダ式には、次の例で示すような名前付きメソッドへの呼び出しを含めることができます。
' Create a task and supply a user delegate by using a lambda expression.
Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))
' Start the task.
taskA.Start()
' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
' Output:
' Hello from the joining thread.
' Hello from taskA.
// Create a task and supply a user delegate by using a lambda expression.
var taskA = new Task(() => Console.WriteLine("Hello from taskA."));
// Start the task.
taskA.Start();
// Output a message from the joining thread.
Console.WriteLine("Hello from the calling thread.");
/* Output:
* Hello from the joining thread.
* Hello from taskA.
*/
StartNew メソッドを使用して、一度の操作でタスクを作成および開始することもできます。 次の例に示すように、この方法は、作成とスケジュール設定を分ける必要がないときにタスクを作成して開始する場合に適しています。
' Better: Create and start the task in one operation.
Dim taskA = Task.Factory.StartNew(Sub() Console.WriteLine("Hello from taskA."))
' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
// Create and start the task in one operation.
var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA."));
// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");
Task は TaskFactory の既定インスタンスを返す静的 Factory プロパティを公開しているので、メソッドを Task.Factory.StartNew(…) として呼び出すことができます。 また、この例のタスクは System.Threading.Tasks.Task<TResult> 型であるため、それぞれのタスクは計算の結果を格納するパブリックな Result プロパティを持ちます。 タスクは非同期に実行され、任意の順序で完了されることがあります。 計算が完了する前に Result にアクセスした場合、このプロパティは値が使用可能な状態になるまでスレッドをブロックします。
Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation1()),
Task(Of Double).Factory.StartNew(Function() DoComputation2()),
Task(Of Double).Factory.StartNew(Function() DoComputation3())}
Dim results() As Double
ReDim results(taskArray.Length)
For i As Integer = 0 To taskArray.Length
results(i) = taskArray(i).Result
Next
Task<double>[] taskArray = new Task<double>[]
{
Task<double>.Factory.StartNew(() => DoComputation1()),
// May be written more conveniently like this:
Task.Factory.StartNew(() => DoComputation2()),
Task.Factory.StartNew(() => DoComputation3())
};
double[] results = new double[taskArray.Length];
for (int i = 0; i < taskArray.Length; i++)
results[i] = taskArray[i].Result;
詳細については、「方法: タスクから値を返す」を参照してください。
ラムダ式を使用してタスクのデリゲートを作成すると、ソース コード内の該当ポイントで参照できるすべての変数にアクセスできます。 ただし、特にループ内では、ラムダによって変数が予想どおりにキャプチャされない場合があります。 ラムダでは、反復処理が実行されるたびに変更された値をキャプチャするのではなく、最終値だけがキャプチャされます。 反復処理が実行されるたびに値にアクセスできるようにするには、次の例に示すように、コンストラクターによって状態オブジェクトをタスクに提供します。
Class MyCustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Sub TaskDemo2()
' Create the task object by using an Action(Of Object) to pass in custom data
' in the Task constructor. This is useful when you need to capture outer variables
' from within a loop.
' As an experiement, try modifying this code to capture i directly in the lamda,
' and compare results.
Dim taskArray() As Task
ReDim taskArray(10)
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = New Task(Sub(obj As Object)
Dim mydata = CType(obj, MyCustomData)
mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId
Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
mydata.Name, mydata.CreationTime, mydata.ThreadNum)
End Sub,
New MyCustomData With {.Name = i, .CreationTime = DateTime.Now.Ticks}
)
taskArray(i).Start()
Next
End Sub
class MyCustomData
{
public long CreationTime;
public int Name;
public int ThreadNum;
}
void TaskDemo2()
{
// Create the task object by using an Action(Of Object) to pass in custom data
// in the Task constructor. This is useful when you need to capture outer variables
// from within a loop. As an experiement, try modifying this code to
// capture i directly in the lambda, and compare results.
Task[] taskArray = new Task[10];
for(int i = 0; i < taskArray.Length; i++)
{
taskArray[i] = new Task((obj) =>
{
MyCustomData mydata = (MyCustomData) obj;
mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
mydata.Name, mydata.CreationTime, mydata.ThreadNum)
},
new MyCustomData () {Name = i, CreationTime = DateTime.Now.Ticks}
);
taskArray[i].Start();
}
}
この状態は、タスク デリゲートに引数として渡されます。AsyncState プロパティを使用することで、タスク オブジェクトから状態にアクセスできます。 また、コンストラクターを使用してデータを渡すと、一部のシナリオでパフォーマンスが若干向上する場合があります。
タスク ID
各タスクは、アプリケーション ドメイン内で一意に識別され、Id プロパティを使用してアクセスできる整数の ID を受け取ります。 この ID は、Visual Studio デバッガーの [並列スタック] ウィンドウおよび [並列タスク] ウィンドウでタスク情報を確認する場合に役立ちます。 この ID は ID が要求されるまでは作成されません。したがって、タスクの ID はプログラムが実行されるたびに異なる場合があります。 デバッガーでタスク ID を表示する方法の詳細については、「[並列スタック] ウィンドウの使用」を参照してください。
タスクの作成オプション
タスクを作成するほとんどの API には、TaskCreationOptions パラメーターを受け入れるオーバーロードが用意されています。 これらのオプションのいずれかを指定することで、タスク スケジューラにスレッド プール上のタスクをスケジュールする方法を指示できます。 タスクの作成オプションの一覧を次に示します。
要素 |
説明 |
---|---|
None |
オプションを指定しなかった場合の既定のオプションです。 スケジューラは既定のヒューリスティックを使用してタスクをスケジュールします。 |
PreferFairness |
先に作成されたタスクが先に実行され、後から作成されたタスクは後から実行されるように、タスクのスケジュールを指定します。 |
LongRunning |
実行に時間のかかる操作を表すタスクであることを示します。 |
AttachedToParent |
子が存在する場合、現在のタスクにアタッチされた子としてタスクを作成するように指定します。 詳細については、「入れ子のタスクと子タスク」を参照してください。 |
このオプションは、ビットごとの OR 演算を使用して組み合わせることができます。 次の例は、LongRunning オプションと PreferFairness オプションが指定されたタスクを示しています。
Dim task3 = New Task(Sub() MyLongRunningMethod(),
TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()
var task3 = new Task(() => MyLongRunningMethod(),
TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();
タスクの継続の作成
Task.ContinueWith メソッドおよび Task<TResult>.ContinueWith メソッドで、継続元タスクが完了したときに開始されるタスクを指定できます。 継続タスクのデリゲートには、タスクの状態を調べることができるように、継続元への参照が渡されます。 さらに、継続元の出力を継続の入力として使用できるようにするため、ユーザー定義の値を Result プロパティで継続元から継続に渡すことができます。 次の例では、getData がプログラム コードによって開始された後、getData の完了時に analyzeData が自動的に開始され、analyzeData の完了時に reportData が開始されます。 getData は結果をバイト配列で生成し、analyzeData に渡します。 analyzeData はその配列を処理し、Analyze メソッドの戻り値の型から推論される型を持つ結果を返します。 reportData は analyzeData から入力を受け取り、同様の方法を使用して (プログラムで使用できるようになったと) 推論される型を持つ結果を Result プロパティで生成します。
Dim getData As Task(Of Byte()) = New Task(Of Byte())(Function() GetFileData())
Dim analyzeData As Task(Of Double()) = getData.ContinueWith(Function(x) Analyze(x.Result))
Dim reportData As Task(Of String) = analyzeData.ContinueWith(Function(y As Task(Of Double)) Summarize(y.Result))
getData.Start()
System.IO.File.WriteAllText("C:\reportFolder\report.txt", reportData.Result)
Task<byte[]> getData = new Task<byte[]>(() => GetFileData());
Task<double[]> analyzeData = getData.ContinueWith(x => Analyze(x.Result));
Task<string> reportData = analyzeData.ContinueWith(y => Summarize(y.Result));
getData.Start();
//or...
Task<string> reportData2 = Task.Factory.StartNew(() => GetFileData())
.ContinueWith((x) => Analyze(x.Result))
.ContinueWith((y) => Summarize(y.Result));
System.IO.File.WriteAllText(@"C:\reportFolder\report.txt", reportData.Result);
ContinueWhenAll メソッドおよび ContinueWhenAny メソッドを使用すると、複数のタスクから継続できます。 詳細については、「継続タスク」および「方法: 複数のタスクを継続に連結する」を参照してください。
デタッチされた入れ子のタスクの作成
タスクで実行中のユーザー コードで新しいタスクを作成し、AttachedToParent オプションを指定しない場合、新しいタスクはどのような方法でも外側のタスクとは同期されません。 このようなタスクは、"デタッチされた入れ子のタスク" と呼ばれます。 次の例は、デタッチされた入れ子のタスクを 1 つ作成するタスクを示しています。
Dim outer = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Detached task completed.")
End Sub)
End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")
' Output:
' Outer task beginning.
' Outer task completed.
' Detached child completed.
var outer = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Detached task completed.");
});
});
outer.Wait();
Console.WriteLine("Outer task completed.");
/* Output:
Outer task beginning.
Outer task completed.
Detached task completed.
*/
外側のタスクは入れ子のタスクの完了を待機しないことに注意してください。
子タスクの作成
タスクで実行中のユーザー コードで AttachedToParent オプションを使用してタスクを作成すると、新しいタスクは親タスクである元のタスクの子タスクになります。 AttachedToParent オプションを使用すると、構成されたタスクの並列化を表現できます。親タスクは、すべての子タスクが完了するのを暗黙的に待機するためです。 次の例は、1 つの子タスクを作成するタスクを示しています。
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Parent task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completed.")
End Sub,
TaskCreationOptions.AttachedToParent)
End Sub)
outer.Wait()
Console.WriteLine("Parent task completed.")
' Output:
' Parent task beginning.
' Attached child completed.
' Parent task completed.
var parent = Task.Factory.StartNew(() =>
{
Console.WriteLine("Parent task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completed.");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent task completed.");
/* Output:
Parent task beginning.
Attached task completed.
Parent task completed.
*/
詳細については、「入れ子のタスクと子タスク」を参照してください。
タスクの待機
System.Threading.Tasks.Task 型と System.Threading.Tasks.Task<TResult> 型には、タスクが完了するまで待機できる Task.Wait メソッドと Task<TResult>.Wait メソッドのオーバーロードがいくつか用意されています。 さらに、静的な Task.WaitAll メソッドおよび Task.WaitAny メソッドのオーバーロードにより、一部またはすべてのタスクの配列が完了するまで待機できます。
通常は、タスクを待機するのは次のいずれかの場合です。
メイン スレッドが、タスクで計算される最終的な結果に依存する。
タスクからスローされる可能性のある例外を処理する必要がある。
次の例は、例外処理を含まない基本的なパターンを示しています。
Dim tasks() =
{
Task.Factory.StartNew(Sub() MethodA()),
Task.Factory.StartNew(Sub() MethodB()),
Task.Factory.StartNew(Sub() MethodC())
}
' Block until all tasks complete.
Task.WaitAll(tasks)
' Continue on this thread...
Task[] tasks = new Task[3]
{
Task.Factory.StartNew(() => MethodA()),
Task.Factory.StartNew(() => MethodB()),
Task.Factory.StartNew(() => MethodC())
};
//Block until all tasks complete.
Task.WaitAll(tasks);
// Continue on this thread...
例外処理を示す例については、「方法: タスクがスローした例外を処理する」を参照してください。
タイムアウトを指定できるオーバーロードおよび別の CancellationToken を入力パラメーターとして受け取るオーバーロードの中には、プログラムによって、またはユーザーの入力に応答して待機自体を取り消すことができるものがあります。
タスクで待機する場合は、TaskCreationOptions AttachedToParent オプションを使用して作成されたタスクのすべての子タスクを暗黙的に待機します。 タスクが既に完了している場合、直ちに Task.Wait が返されます。 タスクで発生した例外は、Wait メソッドがタスクの完了後に呼び出された場合でも Wait メソッドによってスローされます。
詳細については、「方法: 1 つ以上のタスクが完了するのを待つ」を参照してください。
タスクでの例外処理
タスクで 1 つ以上の例外がスローされると、例外は AggregateException でラップされます。 例外はタスクに連結されたスレッドに反映されます。通常は、タスクで待機しているスレッドか、またはタスクの Result プロパティにアクセスを試みるスレッドです。 この動作では、.NET Framework ポリシーが適用され、既定ではすべてのハンドルされない例外によって処理が破棄されます。 呼び出し元のコードでは、タスクまたはタスク グループで Wait メソッド、WaitAll メソッド、WaitAny メソッドまたは Result() プロパティを使用して、Wait メソッドを try-catch ブロックで囲むことで例外を処理できます。
連結しているスレッドでも、タスクがガベージ コレクトされる前に Exception プロパティにアクセスすることで例外を処理できます。 このプロパティにアクセスすると、ハンドルされない例外が、オブジェクトが完成したときにプロセスを廃棄する例外の反映動作をトリガーしないようにできます。
例外およびタスクの詳細については、「例外処理 (タスク並列ライブラリ)」および「方法: タスクがスローした例外を処理する」を参照してください。
タスクの取り消し
Task クラスは他の処理と連携したキャンセル処理をサポートしており、.NET Framework Version 4 の新しい System.Threading.CancellationTokenSource クラスおよび System.Threading.CancellationToken クラスと完全に統合されています。 System.Threading.Tasks.Task クラスの多くのコンストラクターは、CancellationToken を入力パラメーターとして受け取ります。 StartNew オーバーロードの多くも、CancellationToken を受け取ります。
CancellationTokenSource クラスを使用すると、トークンを作成し、後でキャンセル要求を発行できます。 このトークンを Task に引数として渡し、同じトークンをキャンセル要求に応答するユーザー デリゲートで参照します。 詳細については、「タスクのキャンセル」および「方法: タスクとその子を取り消す」を参照してください。
TaskFactory クラス
TaskFactory クラスには、タスクおよび継続タスクの作成と開始について、一般的なパターンをカプセル化する静的メソッドが用意されています。
最も一般的なパターンは、1 つのステートメントで 1 つのタスクを作成および開始する StartNew です。 詳細については、「StartNew()」を参照してください。
複数の継続元から継続タスクを作成する場合は、ContinueWhenAll メソッドまたは ContinueWhenAny メソッドを使用するか、Task<TResult> クラスの同等のメソッドを使用します。 詳細については、「継続タスク」を参照してください。
Task インスタンスまたは Task<TResult> インスタンスで非同期プログラミング モデルの BeginX メソッドおよび EndX メソッドをカプセル化するには、FromAsync メソッドを使用します。 詳細については、「TPL と従来の .NET 非同期プログラミング」を参照してください。
既定の TaskFactory へは、Task クラスまたは Task<TResult> クラス上の静的なプロパティとしてアクセスできます。 TaskFactory を直接インスタンス化し、さまざまなオプションを指定することもできます。たとえば、CancellationToken、TaskCreationOptions オプション、TaskContinuationOptions オプション、TaskScheduler などです。 タスク ファクトリを作成するときに指定されるオプションは、タスク ファクトリで作成したすべてのタスクに適用されます。ただし、タスクが TaskCreationOptions 列挙型を使用して作成された場合は例外で、タスクのオプションによってタスク ファクトリのオプションがオーバーライドされます。
デリゲートなしのタスク
Task を使用して、固有のユーザー デリゲートではなく外部コンポーネントによって実行される非同期操作をカプセル化する場合があります。 操作が非同期プログラミング モデルの Begin/End パターンに基づいている場合、FromAsync メソッドを使用できます。 そうでない場合は、TaskCompletionSource<TResult> オブジェクトを使用して、タスク内の操作をラップして、Task を外部からプログラミング可能にする利点を活用できます。たとえば、例外の反映および継続のサポートです。 詳細については、「TaskCompletionSource<TResult>」を参照してください。
カスタム スケジューラ
アプリケーションまたはライブラリのほとんどの開発者は、タスクを実行するプロセッサ、他のタスクと動作を同期する方法、System.Threading.ThreadPool でスケジュールする方法などについては気にしません。 気にするのは、ホスト コンピューター上でできるだけ効率的に実行することだけです。 スケジュールの詳細についてより詳細に制御する必要がある場合、タスク並列ライブラリでは、既定のタスク スケジューラの設定を構成でき、さらにカスタム スケジューラを利用することもできます。 詳細については、「TaskScheduler」を参照してください。
関連のデータ構造
TPL には、並列のシナリオおよび順次的なシナリオの両方に役立つ複数の新しいパブリック型があります。 これらの型には、System.Collections.Concurrent 名前空間における、スレッド セーフで、高速、スケーラブルなコレクション クラス、および新しい同期の型が含まれます。たとえば、SemaphoreLock および System.Threading.ManualResetEventSlim は、特定の種類の作業負荷に関しては、先行タスクより効率的です。 その他の .NET Framework Version 4 の新しい型には、System.Threading.Barrier と System.Threading.SpinLock があり、以前のリリースでは利用できなかった機能が用意されています。 詳細については、「並列プログラミングのデータ構造」を参照してください。
カスタムのタスクの型
System.Threading.Tasks.Task または System.Threading.Tasks.Task<TResult> から継承しないことをお勧めします。 代わりに、AsyncState プロパティを使用して、追加のデータまたは状態を Task オブジェクトまたは Task<TResult> オブジェクトに関連付けます。 拡張メソッドを使用して、Task クラスおよび Task<TResult> クラスの機能を拡張することもできます。 拡張メソッドの詳細については、「拡張メソッド (C# プログラミング ガイド)」および「拡張メソッド (Visual Basic)」を参照してください。
Task または Task<TResult> から継承する必要がある場合、System.Threading.Tasks.TaskFactory、System.Threading.Tasks.TaskFactory<TResult>、System.Threading.Tasks.TaskCompletionSource<TResult> の各クラスを使用して、カスタムのタスクの型のインスタンスを作成することはできません。これらのクラスで作成されるのは、Task オブジェクトと Task<TResult> オブジェクトだけであるためです。 また、Task、Task<TResult>、TaskFactory、および TaskFactory<TResult> で提供されるタスク継続機構でも、Task オブジェクトと Task<TResult> オブジェクトしか作成されないため、これらの機構を使用してカスタムのタスクの型のインスタンスを作成することはできません。
関連トピック
タイトル |
説明 |
継続の機能について説明します。 |
|
子タスクと入れ子にされたタスクの違いについて説明します。 |
|
Task クラスに組み込まれているキャンセルのサポートについて説明します。 |
|
同時実行スレッド上の例外を処理する方法について説明します。 |
|
Invoke の使用方法について説明します。 |
|
タスクから値を返す方法について説明します。 |
|
タスクで待機する方法について説明します。 |
|
タスクを取り消す方法について説明します。 |
|
タスクがスローした例外を処理する方法について説明します。 |
|
別のタスクが完了したときにタスクを実行する方法について説明します。 |
|
タスクを使用してバイナリ ツリーを走査する方法について説明します。 |
|
.NET 並列プログラミングのトップ レベル ノード。 |
参照
概念
履歴の変更
日付 |
履歴 |
理由 |
---|---|---|
2011 年 3 月 |
Task クラスと Task<TResult> クラスから継承する方法に関する情報を追加。 |
情報の拡充 |