非同期メソッドとasync/awaitキーワード
- 非同期処理でありながら、コールバック記述をする事なく、awaitキーワードを使って完了待機を記述できる(同期的に記述できる)。
- 「非同期メソッド」とは シグネチャにasyncを付けた特別なメソッドのことで、asyncで修飾されたメソッドを「非同期メソッド」と呼びます。別の言い方を言えば、awaitしたい場合はasyncでメソッドを修飾して非同期メソッドにしなければならない。
- async修飾子はただの目印でしかなく、コンパイル結果は通常のメソッドと変わりません。なので、async修飾子を付加したからと言って、そのメソッド内の処理が、別スレッド上で非同期的に動作することにならない。よって、非同期メソッド内のタスク実行以外の処理は、UIスレッドで動作するためコントロールに関する処理を記述できる。
- async修飾子の意味は、「このメソッドは非同期操作を待つ必要がある制御フロー (await) を含んでおり、非同期処理の適切な時点でこのメソッドを、再度、途中から始められるようにコンパイラによって継続渡しに書き直される」ことを意味する。
- await演算子の意味は「待っているタスクがまだ完了していない場合、このメソッドの残りをそのタスクの継続として登録して呼び出し元に処理を戻す。タスクが完了したら、タスクは登録しておいた継続を実行する」。「非同期処理が終了するまで呼び出し元スレッドをブロックして待機する」という意味ではないので注意。
- あるメソッド内(MethodA)の処理内で非同期メソッドを使用する場合、MethodAにasyncキーワードを付加することによって、MethodAを呼び出す側にMethodA内で非同期メソッドが使用されていることを知らせ、MethodAの呼出し側で待たせるor待たせないを選択可能とさせる。
- 非同期メソッドの戻り値は、通常、Task/Task<T>になる。return task/task<T>のような記述はないが、メソッド内の処理をタスクとして生成して、メソッドの戻り値として返している。async修飾子の付いた関数はreturn文がなくてもUIスレッドをタスク化したもの、もしくは継続タスクが返るよう。非同期メソッドから「return 10;」のように値を返す場合は、戻り値の型をTask<T>型にする必要がある。
- return文を書かない場合には戻り値にvoidを指定できる。戻り値にvoidを指定するケースは、呼び出し側が非同期メソッドの完了を待機しない場。例えばGUIアプリケーションのイベントハンドラなど。よって、戻り値がvoid型の非同期メソッドは、待機(継続渡し)が実行されない。void型の記述はイベントハンドラへの適用のみに留めるのがベストプラクティスとされている。
- メソッド内のreturnはタスクの終了を示す。Task<T>の場合はreturn T;でタスクの結果の戻すことができる(Resultプロパティで取得する)。
- 非同期メソッドの特徴はただ一つ、メソッド内でawaitキーワードを使えるようになること。
- awaitキーワードの効果は、①「ロックされず指定したTaskの完了を待つ」②「タスク完了後に結果を取り出す」こと。
- 非同期メソッドとは、複数の「タスク」の実行順序などを記述した「一つのタスク」である。メソッドの記述でタスクを生成している感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//非同期メソッド(タスク結果を返す場合) //当該メソッド内の処理で非同期メソッドを使用する場合、asyncキーワードによって、 //当該メソッドを呼び出す側に非同期メソッドが使用されていることを知らせ、 //当該メソッド実行時に待たせるor待たせないを選択可能とさせる。 private async Task<string> AsyncMethod1(Uri uri) { using (var client = new HttpClient()) // 本当はHttpClientをusingで使っちゃダメ { //重い処理。sleepはタスクで実行されているわけでないので、メインスレッドで実行されロックされる sleep(10000); //非同期メソッドの実行。「client.GetAsync」タスクを開始し、その完了を待機する var response = await client.GetAsync(uri); //タスクで実行されるので、ロックされない //非同期メソッドの実行。「response.Content.ReadAsStringAsync」タスクを開始し、その完了を待機する string text = await response.Content.ReadAsStringAsync(); //タスクで実行されるので、ロックされない // 戻り値の文字列を加工して戻す string response = Something1(text); return response; } } //※「一つのタスク(Task)」を表す。 //※return taskのような記述はないが、メソッド内の処理をタスク生成して、メソッドの戻り値として返している。 //呼出し方 Task<string> task = AsyncMethod1(Uri); await task; Console.writeLine(task.Result) //上記をまとめて記述した方法 string res = await AsyncMethod1(Uri); Console.writeLine(res) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//非同期メソッド(タスク結果を返さない場合) public async Task RunHeavyMethodAsync2() { //重い処理。sleepはタスクで実行されているわけでないので、メインスレッドで実行されロックされる sleep(10000); // 「HeavyMethodを実行する」というタスクを開始し、完了するまで待機 await Task.Run(() => HeavyMethod2(x)); // 別の処理 something(); } //※return taskのような記述はないが、メソッド内の処理をタスクとして生成して、メソッドの戻り値として返している。 //呼出し方 Task task = RunHeavyMethodAsync2(); await task; //上記をまとめて記述した方法 await RunHeavyMethodAsync2(); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//非同期メソッド(タスク結果を返す場合) public async Task<string> RunHeavyMethodAsync3() { // 「文字型の戻り値のHeavyMethodを実行する」というタスクを開始し、完了するまで待機 string textB = await Task<string>.Run(() => { //重い処理。sleepはタスクで実行されているので、メインスレッドからロックされない sleep(10000); //通常メソッドの実行 string textA = HeavyMethod3(y) // 戻り値の文字列を加工して戻す string responseA = SomethingA(textA); return responseA; }); // 戻り値の文字列を加工して戻す string responseB = SomethingB(textB); return responseB; } //※return taskのような記述はないが、メソッド内の処理のタスクとして生成して、メソッドの戻り値として返している。 //呼出し方 Task<string> task = RunHeavyMethodAsync3(); await task; Console.writeLine(task.Result) //上記をまとめて記述した方法 string res = await RunHeavyMethodAsync3(); Console.writeLine(res) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//タスクを生成するメソッド public Task<string> RunHeavyMethodTask4() { // 「文字型の戻り値のHeavyMethodを実行する」というタスクを開始し、完了するまで待機 var task = Task<string>.Run(() => { //重い処理。sleepはタスクで実行されているので、メインスレッドからロックされない sleep(10000); //通常メソッドの実行 string text = HeavyMethod4(y) // 戻り値の文字列を加工して戻す string response = Something4(text); return response; }); return task; } //呼出し方 Task<string> task = RunHeavyMethodTask4(); await task; Console.writeLine(task.Result) //上記をまとめて記述した方法 string res = await RunHeavyMethodTask4(); Console.writeLine(res) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//タスクを生成するメソッド public Task<string> RunHeavyMethodTask5() { // 「文字型の戻り値のHeavyMethodを実行する」というタスクを開始し、完了するまで待機 var task = Task<string>.Run(async () => { //重い処理。sleepはタスクで実行されているので、メインスレッドからロックされない sleep(10000); //非同期メソッドの実行 string text = await HeavyMethod5(z); // 戻り値の文字列を加工して戻す string response = Something5(text); return response; }); return task; } //呼出し方 Task<string> task = RunHeavyMethodTask5(); await task; Console.writeLine(task.Result) //上記をまとめて記述した方法 string res = await RunHeavyMethodTask5(); Console.writeLine(res) |
非同期メソッドを待機しない場合の警告を消す方法
- 非同期メソッドを呼び出す際は、多くの場合 await してその場で待機することが多いですが、待たなくても良い場合もあります。
- そのような場合、await しなければ良いのですが、ただ await しないだけだと 「この呼び出しを待たないため、現在のメソッドの実行は、呼び出しが完了するまで続行します。呼び出しの結果に ‘await’ 演算子を適用することを検討してください。」 という警告が出てしまう。
- 警告は消すコーディングの方法について
1 2 3 4 5 6 |
//非同期タスクの定義 private async static Task TestAsync(string no) { await Task.Delay(1000); Console.WriteLine($"{no}TestAsyncメソッド"); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//警告が表示されるパターン private void Run1() { TestAsync("Run1"); } //待機するパターンなので、趣旨にあっていあい・・・・ private async void Run2() { await TestAsync("Run2"); } //警告は表示されない private void Run3() { var t = TestAsync("Run3"); } //警告は表示されない private void Run4() { Func f = async () => { await TestAsync("Run4"); }; f(); } //警告は表示されない private void Run5() { TestAsync("Run5").ContinueWith(_ => { ; }) ; } |
参考にさせて頂いたページ