2月第2週の振り返り

2月第2週(2/3-2/9)

ASP.NET Core MVC

C#におけるasync/awaitの仕組みについて理解を試みた。

async修飾子のついたメソッドは基本的にTask(返される値がない)またはTask<最終的に返される値の型>を返すこととされ、またメソッド内でawaitの記述が可能となる。

awaitは別スレッドで実行されているTask/Task<T>の処理が終わるまでメインスレッドはその先に進まないでおくという挙動を示す。これを「待つ」と表現するとブロッキングのそれと混同しかねないので避けるべきだと思った。

非同期とは呼び出し元に戻ってきたときにまだ「最終的に期待する結果(Taskならば処理の完遂、Task<T>ならばreturn T)」が得られていないことを意味する。いつその結果が得られるかは処理の内容による。そうすると最終結果を利用してさらに何かを行いたい時に支障が生じる。そこで結果を利用する処理を行う前にawaitを行うと、最終的に期待する結果が得られたことを保証できる。

よくある無意味なawaitは以下のようなものである。

public async Task<int> DoHeavyCalc(int a, int b){
    // この間I/Oや通信、重い計算などが行われる。仮に5秒かかるとする
    return a+b;
}
int calcResult1 = await DoHeavyCalc(3, 5);
int calcResult2 = await DoHeavyCalc(7,12);
// 時間がかかるだろうから他の軽い処理をここでしておく
System.Console.WriteLine($"calcResult1 is {calcResult1}, calcResult2 is {calcResult2}");

このコードでは

  1. 別スレッドでDoHeavyCalc(3, 5)を実行させ、その最終結果が得られるまで先に進まないでおく
  2. 次に別スレッドでDoHeavyCalc(7, 12)を実行させ、その最終結果が得られるまで先に進まないでおく
  3. 他の軽い処理を行う
  4. 2つのintを踏まえてコンソール出力を行う

ので最低10秒かかる。これでは(メインスレッドが固まることがないとはいえ)逐次実行しているのと変わらない。いよいよ処理が完了している必要があるというタイミングになるまでawaitをするべきではなく、従ってこう書く。

Task<int> task1 = DoHeavyCalc(3, 5);
Task<int> task2 = DoHeavyCalc(7, 12);
// 時間がかかるだろうから他の軽い処理をここでしておく
int calcResult1 = await task1;
int calcResult2 = await task2;
// またはint[] calcResults = await Task.WhenAll(task1, task2);
System.Console.WriteLine($"calcResult1 is {calcResult1}, calcResult2 is {calcResult2}");

この場合、

  1. 別スレッドでtask1の実行が開始され、呼び出し元に直ちに戻る
  2. 別スレッドでtask2の実行が開始され、呼び出し元に直ちに戻る
  3. 他の軽い処理を行う
  4. task1の処理が終わるまで先に進まないでおく(1の時点から5秒経つまでこのフェーズ)
  5. task2の処理が終わるまで先に進まないでおく(1と2はほぼ同時に開始しているのでこのフェーズは一瞬)
  6. 2つのintを踏まえてコンソール出力を行う

となり、所要時間は約5秒となる。

元々並列プログラミングのMPI通信関数を扱うべく非同期概念を理解しようと努めていたので、あまり混乱なくC#のasync/awaitを用いることができた。

来週はSignalRを利用したリアルタイム通信について学んでいきたい。