はじめに
業務でC#を使用してAPIの実装を行っており、その中で「Task」「async/await」などをよく目にします。
これらは非同期処理を行うために必要なのですが、理解が浅いため学習メモとして書き残します。
まずは、そもそもの「同期処理・非同期処理ってなに」というところから調べていきます。
同期処理とは?
複数の処理を実行する場合に、1つずつ順番に処理を実行していく
方式のことです。
1つずつ順番に
になので、 実行中の処理が完了されるのを待ってから次の処理が開始されます。
日常生活での例
家事を同期処理のように行うと以下のようになります。
洗濯物を回す(その場で洗濯が完了するまで待機)
↓
洗濯が完了したら、部屋の掃除をする
↓
掃除が完了したら、料理を開始(料理がすべて完了するまで待機)
大雑把に処理を分けましたが、とても非効率だと感じると思います。
同期処理のコード例
static void Main()
{
Console.WriteLine("家事を開始");
WashClothes(); //洗濯
CleanRoom(); //部屋掃除
CookMeal(); //料理
Console.WriteLine("家事を終了");
}
static void WashClothes()
{
Console.WriteLine("洗濯を開始");
Thread.Sleep(10000); //10秒待機
Console.WriteLine("洗濯を終了");
}
static void CleanRoom()
{
Console.CleanRoom("部屋掃除を開始");
Thread.Sleep(5000); //5秒待機
Console.CleanRoom("部屋掃除を終了");
}
static void CookMeal()
{
Console.CookMeal("料理を開始");
Thread.Sleep(10000); //10秒待機
Console.CookMeal("料理を終了");
}
//全体の所要時間
//10秒(洗濯) + 5秒(掃除) + 10秒(料理)= 25秒
//1つずつ順番に実行される
非同期処理とは?
複数の処理を実行する場合に、並行して処理を実行していく
方式のことです。
同期処理は実行中の処理が完了されるまで待機する必要がありましたが、非同期処理では処理の完了を待たずに他の処理を実行することができます。
日常生活での例
洗濯物を回す
↓
洗濯されている間に部屋の掃除を行う
↓
掃除が完了したら料理を始める(洗濯は実行中)
↓
料理完成、洗濯も完了
なぜ非同期処理が必要?
上記の「日常生活での例」を見て、時間のかかる処理を同期的に行うととても非効率だということが分かりました。
プログラミングでも、WebAPIの呼出しやDBへのアクセスなど時間のかかる処理が多く存在します。
時間のかかる処理を同期的に実行すると、処理の完了まで非常に時間がかかります。このことが原因でアプリがフリーズしているように見えてしまい、UXの著しい低下に繋がります。
上記の問題を解決するためにも、並行して複数の処理が実行できる非同期処理は欠かせないということです。
非同期処理のコード例
static async Task Main()
{
Console.WriteLine("家事を開始");
// 並行して進める
Task washTask = WashClothesAsync();
Task cleanTask = CleanRoomAsync();
Task cookTask = CookMealAsync();
//全ての処理が完了するまで待機
await Task.WhenAll(washTask, cleanTask, cookTask);
Console.WriteLine("家事を終了");
}
static async Task WashClothesAsync()
{
Console.WriteLine("洗濯を開始");
await Task.Delay(10000); //10秒待機(非同期)
Console.WriteLine("洗濯を終了");
}
static void CleanRoomAsync()
{
Console.CleanRoom("部屋掃除を開始");
await Task.Delay(5000); //5秒待機(非同期)
Console.CleanRoom("部屋掃除を終了");
}
static void CookMealAsync()
{
Console.CookMeal("料理を開始");
await Task.Delay(10000); //10秒待機(非同期)
Console.CookMeal("料理を終了");
}
//全体の所要時間
//最も時間がかかる処理(洗濯・料理)= 10秒
//3つの処理が並行して実行されるので、効率的
Task、async/awaitについて
Taskとは?
Taskとはなにか、とりあえず公式を確認します。
非同期操作を表します
シンプルすぎる説明ですが、非同期処理で必要になるクラスのようです。
もう少しTaskについて知りたいので、参考資料から一部引用させていただきます。
「Taskとは、『非同期処理』のことではない。」
だって、「Task」はただの「タスク」なのだから。
Taskクラスはその名の通り、一つの「作業単位」「仕事」「タスク」そのものを表しているといえます。
C#のTaskとはTaskです。
これ自体は非同期処理でもなんでもない、ただの処理の手順書に過ぎません。
引用元の資料を見ても正直難しい...正しく理解できているか不安ですが、
つまり、Taskとは「ただのタスク・処理手順書であり、非同期処理を実行するものではなく、非同期処理の進行状況や結果を管理するもの」と言えるのではないでしょうか。
そしてTaskを使用することで、以下のことが実現できるようです。
- 非同期処理の進行状況(完了したか?エラーが起きたか?など)を知れる
- 非同期処理の結果を受け取れる
- 複数の非同期処理を組み合わせることができる
async/awaitとは?
async/awaitとは、Taskを非同期で実行するために必要なキーワードであり、非同期処理をわかりやすく、簡単に扱うための仕組みと言えます。
async/awaitを使用することで、非同期処理を同期処理のように書くことができるので、シンプルでスッキリとしたコードを書けるメリットがあります。
それぞれの役割は以下になります。
キーワード | 役割 |
---|---|
async | メソッドを「非同期メソッド」として定義できる |
await | 非同期処理の完了を待つ |
async
をメソッドのシグネチャに付与することで非同期メソッドとして定義できて、付与したメソッド内でawait
を使用できるようになります。
await
を使用すると指定した非同期処理の完了を待機することができます。指定した非同期処理が完了すればawait
以降の処理は実行されていきます。
await
で待機しておけば、メインスレッドはブロックされないので、画面がフリーズしているように見える問題も解決できます。
非同期処理で注意すること
非同期処理は便利で開発において必要不可欠ですが、実装を間違えると不具合が多発して沼にはまってしまいます。
以下はこちらの記事から一部引用させていただいたものになります。
1.Task.Run()を使ってはいけません
2.Task.Result, Task.Wait()を使ってはいけません
3.Threadクラスを使ってはいけません
4.async-await構文だけを使って書きます
5.async voidにするのは特殊な場合だけです
非同期処理の沼にハマる様子をストーリー形式で紹介されていて勉強になりました。
上記を参考に、注意点と理由を簡単にまとめてみました。
Task.Run()は使用しない
理由
- Task.Run()はCPUバウンド処理(計算負荷の高い処理)を別スレッドで処理したいときに使う。
- 基本的にはawaitを使用すればTask.Run()は不要。
Task.ResultやTask.Wait()も使用しない
理由
- ResultやWait()を使用すると、UIスレッドがある環境ではデッドロックの原因になるから。
- awaitを使用して待機するようにする。
Threadクラスも使用しない
理由
- ThreadだとTaskよりも複雑な実装になってしまう。
- Threadはレガシー?な非同期処理方法なので、Task、async/awaitを使用していく。
async/awaitだけを使用する。同期処理は混ぜない。
理由
- デッドロックの原因にもなり、コードの可読性も悪くなる。
- async/awaitだけを使用して実装を行う。
async void も極力使用しない
理由
- 戻り値(Task)を返さないため、Taskの状態を把握できない。
-
async void
で定義したメソッド内でawait
を使用してもTaskを返さないので、例外処理やエラーハンドリングが難しい(VSではそもそもエラーが出る)。 - 例外としてイベントハンドラ(ボタンのクリック処理など)には使用してもいい。イベントハンドラは戻り値がvoidでないとダメだから。
おわりに
色々調べましたがやはり非同期処理難しい…個人的には特にTaskについてまだ理解が浅いと感じます。
C#以外の言語でも非同期処理への理解は必須だと思うので、引き続き学習を続けていこうと思います。