ホームに戻る
出典 :
関連 :
目次 :

.NETにおける並列処理

.NETでは並列処理(マルチタスク)がサポートされている。 過去(.NET Framework 1 ~ 3) Thread 、ThreadPool 、BackgroundWorker といったクラスを用いて並列処理を実現していたが、これらは扱いが煩雑で習得難度も高かった。 .NET Framework 4で Task クラスが登場し、比較的容易に並列処理を実現できるようになった。 本記事ではこの Task を用いることを前提として解説を行う。

タスクの開始と完了待ち

Task.Run()

Task task = Task.Run( () => HeavyWork(2000000000) );
Task.Run() は引数で指定された処理を別スレッドで開始する。 引数はデリゲート( Action (戻り値なしのメソッド)または Func (戻り値のあるメソッド) )で、デルタ式も使用できる。 スレッドの終了待ちや実行結果(戻り値)の取得は戻り値の Task<T> に対して行う。 ここで T は Task.Run() に渡されるメソッド(ここでは HeavyWork() )の戻り値の型である。戻り値が存在しない( void )場合は単に Task となる。 また Task.Run() は指定された処理をスレッドプールに投入するのみで、通常、その処理をどのスレッドで実行するのかは問わない。

async / await

非同期処理の完了を待つ。後述の Wait() と異なり、スレッドをブロックしない点が特徴。
// メソッド内で await を用いるため async 修飾 // 本来の戻り値は void だが、async 修飾されているため Task に変更する private async Task ExecuteAsync() { // (1) text += "計算開始" + Environment.NewLine; // (2) 重い処理を別タスクで実行開始(Task.Run()) // await 修飾されているため、次行以降は別タスクの完了後に実行される double result = await Task.Run(() => HeavyWork(2000000000)); // (3) text += result.ToString() + Environment.NewLine; }
画像
上記は async / await を用いた非同期処理の例である。シーケンス図中の塗りつぶし(薄黄薄青)はスレッドを示す。 await キーワードに到達すると後続のタスクを別スレッドで開始するとともに、呼び元に制御を戻すことが重要な点としてあげられる。 時間のかかる処理を行う場合でも呼び元をブロックしないため、特に呼び元がUIスレッドの場合に有効である。 (UIスレッドがブロックされるとユーザ操作を受け付けることができなくなる。) また、await はタスクの終了を待機するため、後続行(コード中の (3) )は、待機したタスクの終了後に実行される。 このように、比較的少ない記述で非同期処理を実現することができる。 ここで、メソッド(ここでは ExecuteAsync() )中で await を用いる場合、メソッドを async 修飾する必要があるとともに、 戻り値を Task とする必要がある。
ただし、(UI)イベントハンドラは例外的に async void を用いることができる。 上記の例でもわかるように、async 修飾を行った場合でも明示的に Task を返す必要はない。

応用

HttpClient hc = new HttpClient(); : private async void button_Click(object sender, RoutedEventArgs e) { // ダウンロードタスクを別スレッドで実行 // ( Task オブジェクトに格納) Task<string> gethtmltask = hc.GetStringAsync("http://www.example.jp/kussoosoi.php"); // この行は非同期タスクの終了を待たずに実行される MessageBox.Show("HTMLを取得しています"); // ダウンロード開始 // 非同期タスクの完了を待機し、結果を取得する textBox.Text = await gethtmltask; }
開始した非同期タスクを変数に格納しておき、あとで await する手法。
メッセージボックス表示の裏でHTTP通信を行うので、体感的な待ち時間の短縮が期待できる。

Wait() / Result / WaitAll() / WaitAny()

HttpClient hc = new HttpClient(); // 非同期実行開始 Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/"); Task<string> t2 = hc.GetStringAsync("https://www.bing.com/"); // t1 が終わるまでスレッドをブロックする t1.Wait(); // t2 が終わるまでスレッドをブロックし、結果を取得する string binghtml = t2.Result; // いずれかの Task が終わるまでスレッドをブロックする // 0 : t1 が先に終了 // 1 : t2 が先に終了 int completedTaskIndex = Task.WaitAny(t1, t2); // いずれかの Task が終わるまでスレッドをブロックする(タイムアウト付き) // 0 : t1 が先に終了 // 1 : t2 が先に終了 // -1 : いずれも終了しないまま50ms経過 int completedTaskIndex2 = Task.WaitAny(new[] { t1, t2 }, 50); // すべての Task が終わるまでスレッドをブロックする Task.WaitAll(t1, t2); // すべての Task が終わるまでスレッドをブロックする(タイムアウト付き) // true : いずれも50ms以内に終了 // false : いずれかが終了しないまま50ms経過 bool allTasksCompleted = Task.WaitAll(new[] { t1, t2 }, 50);
await と異なり、呼び元に制御を戻さない。

WhenAll() / WhenAny()

タスクの終了通知を受け取る。複数のタスクを await する際に用いる。戻り値が異なる点に注意。
HttpClient hc = new HttpClient(); Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/"); Task<string> t2 = hc.GetStringAsync("https://www.bing.com/"); // スレッドをブロックせずにすべてのタスクの終了を待機 string[] htmls = await Task.WhenAll(t1, t2); // -> htmls[0] : t1 の実行結果 // htmls[1] : t2 の実行結果 // スレッドをブロックせずにいずれかのタスクの終了を待機 Task<string> completedTask = await Task.WhenAny(t1, t2);

IsCompleted / IsCompletedSuccessfully / IsFaulted / IsCanceled

タスクが終了しているかを示すプロパティ。
HttpClient hc = new HttpClient(); Task<string> task = hc.GetStringAsync("https://www.microsoft.com/"); // タスクが完了していなければメッセージ表示 if (!task.IsCompleted) { Console.WriteLine("ちょっとまってね"); } // タスクの完了を待機 var html = await task; Console.WriteLine(html);

例外の捕捉

並列処理においては、例外の捕捉には制約がある。詳細は リンク先を参照。