C Sharpの基礎 - マルチスレッド
概要
.NET Framework 4.5で導入されたC# 5.0(Visual Studio 2012) からは、マルチスレッドプログラミングをコンパイラレベルでサポートしている。
具体的には、asyncとawaitというキーワードが導入された。
これは、タスクベース非同期パターン(Task-based Asynchronous Pattern、TAP)を実装するものである。
このタスクベース非同期パターンでは、処理の開始はMethodNameAsyncという形式をとる。
そしてその戻り値を、Task、Task<T>、voidとし、Taskを待つにはawaitを付加する。
また、awaitを付加した関数には、asyncを付加する。
下記の例は、Task<int>型を返す非同期関数である。
 public async Task<int> MethodAsync()
 {
    var iRet = await Task.Run(() =>
                     {
                        await Task.Delay(5000);
                        return 0;
                     });
    return iRet;
 }
asyncとawaitの動作
Taskは、一連の処理をひとまとまりにした単位である。
Taskを呼び出した際はマルチスレッドで実行されるようになっており、連続で記述する場合は、記述した個数だけマルチスレッドで並列処理される。
 public async Task MethodAsync()
 {
    var task1 = Task.Run(() =>
                {
                   // 何か処理1
                });
 
    var task2 = Task.Run(() =>
                {
                   // 何か処理2
                });
 
    var task12  = Task.WhenAll(task1, task2);
 
    var taskAll = task12.ContinueWith(() =>
                  {
                     // 何か処理3
                  });
 
    return await taskAll.ConfigureAwait(false);
 }
Wait / Result
コンソールソフトウェアにおいて、WaitメソッドおよびResultメソッドを使用して、非同期メソッドを作成することができる。
 HttpClient hc = new HttpClient();
 
 string html = hc.GetStringAsync("http://example.jp/").Result;
また、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.Wait();
 
 // タスクが終わるまでスレッドをブロックして結果を取得
 string binghtml = t2.Result;
 
 // どれかのタスクが終わるまでスレッドをブロック
 int completedTaskIndex = Task.WaitAny(t1, t2);
 // 0 : タスクt1の方が速い場合
 // 1 : タスクt2の方が速い場合
 
 // どれかのタスクが終わるまでスレッドをブロック(タイムアウトあり)
 int completedTaskIndex2 = Task.WaitAny(new[] { t1, t2 }, 50);
 // 0  : タスクt1の方が速い場合
 // 1  : タスクt2の方が速い場合
 // -1 : タスクt1およびt2とも50[ms]以内に応答がない場合
 
 // 全てタスクが終わるまでスレッドをブロック
 Task.WaitAll(t1, t2);
 
 // 全てのタスクが終わるまでスレッドをブロック(タイムアウトあり)
 bool allTasksCompleted = Task.WaitAll(new[] { t1, t2 }, 50);
 // true  : タスクt1およびt2とも50[ms]以内に応答がある場合
 // false : タスクt1またはt2のいずれか一方、または、タスクt1およびt2とも50[ms]以内に応答がない場合
WhenAny
複数のタスクのうち、いずれか1つのみが完了するまで待機する場合、WhenAnyメソッドを使用する。
 HttpClient hc = new HttpClient();
 
 Task<string> t1 = hc.GetStringAsync("https://www.microsoft.com/");
 Task<string> t2 = hc.GetStringAsync("https://www.bing.com/");
 
 Task<string> completedTask = await Task.WhenAny(t1, t2);
これを応用して、処理継続中のタスクをキャンセルすることもできる。
 HttpClient hc = new HttpClient();
 CancellationTokenSource cts = new CancellationTokenSource();
 
 // GetStringAsyncメソッドにはCancellationToken構造体を受けるオーバーロードが無いため、GetAsyncメソッドを使用している
 Task<HttpResponseMessage> t1 = hc.GetAsync("https://www.microsoft.com/", cts.Token);
 Task<HttpResponseMessage> t2 = hc.GetAsync("https://www.bing.com/", cts.Token);
 
 // 完了したタスクを取得
 Task<HttpResponseMessage> completedTask = await Task.WhenAny(t1, t2);
 
 // 他の処理中のタスクは全てキャンセルする
 cts.Cancel();
 
 // 完了したタスクの結果を取得
 HttpResponseMessage msg = await completedTask;
 string html = await msg.Content.ReadAsStringAsync();
Task.WhenAllとContinueWith
TaskはContinueWith関数を利用して、その後の処理を連続して実行できる。
2つ以上のスレッドを実行して全スレッドの処理が終了した後、続けてスレッドの処理を書く場合は、
ContinueWithまたはawaitを使用してスレッドの終了を待つ。
 // パターン1
 var task1 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var task2 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var task3 = Task.Run(() =>
             {
                // 何か処理
             });
 
 var taskAll = Task.WhenAll(task1, task2).ContinueWith(task3);  // awaitは不要
 // パターン2
 var task1 = Task.Run(() =>
             {
                // 何か処理
             });
 
 await task1.ConfigureAwait(false);
 
 var task2 = Task.Run(() =>
             {
                // 何か処理
             });
 
 await task2.ConfigureAwait(false);
 
 var task3 = Task.Run(() =>
             {
                // 何か処理
             });
 
 await task3.ConfigureAwait(false);
状況にもよるが、処理が膨大になると可読性が落ちるので、
先に関数で纏めて処理を記述して関数を呼ぶだけでタスク間の関係性が見通せるパターン1の方が良い。
ConfigureAwait(false)
GUIアプリケーションでは、以下のように記述すると、簡単にデッドロックが発生する。
なぜなら、TaskのWaitメソッドやResultは、元のスレッドへ戻ろうとするからである。
以下の場合、methodAsync().Wait()を呼んだ時点で親スレッドがスリープする。
なぜなら、methodAsync()の終了後に親スレッドに戻ろうとするが、既に親スレッドがスリープしているため、
処理を続けることができないからである。
その結果、親スレッドは子スレッドを待ち続け、子スレッドは親スレッドに戻ろうとして、デッドロックが発生する。
 // デッドロックが起きるサンプルコード(GUIアプリケーションのみ)
 // コンソールアプリケーションでは正常に動作する
 public void callMethod()
 {
    methodAsync().Wait();
 }
 
 public async Task methodAsync()
 {
    return await Task.Delay(1000);
 }
対策として、以下のように記述するとデッドロックが回避できる。(戻るスレッドはどこでもよい場合)
 // デッドロックが起きないサンプルコード(GUIアプリケーションのみ)
 public void callMethod()
 {
    methodAsync().Wait();
 }
 
 public async Task methodAsync()
 {
    return await Task.Delay(1000).ConfigureAwait(false);
 }