async/await を完全に理解する
この記事ではasync/await
について完全に理解する[1]ことを目標に説明します。
完全に理解するために一部厳密には正確ではない表現をしますがご了承ください。
(明らかに事実と異なる内容が含まれている場合はご指摘いただけると助かります)
ちなみにC#の文法説明になりますが、他の言語でも基本的な考え方は同じはずです。例えばJavaScriptの場合はTask
型をPromise
型に置き換えていい感じに読んでください。
Task
型
非同期処理とasync/await
を完全に理解するためには、非同期処理の扱い方について完全に理解する必要があります。
そもそも非同期処理って何?という方はググってください。同期処理と非同期処理の違いについては完全に理解した前提で説明します。
非同期処理は複数のタスクを同時並行に処理するためのものですが、実際にコードを書いていると非同期で処理しているタスクが終わるまで待ちたいケースが圧倒的に多い気がします。
public int RunTaskA()
{
RunTaskBAsync(); // 非同期に処理される
// taskBが終わってから続きがしたい・・・
}
タスクB(RunTaskBAsync
)が非同期処理の場合、このままではタスクBが終わる前に続きが処理されてしまいます。
タスクAがタスクBの完了を待ちたい場合、これらの状態を知りたいはずです。
- 正常終了したかどうか
- 例外が発生して異常終了したかどうか
- 処理が途中でキャンセルされたかどうか
また、タスクBに戻り値がある場合、タスクBが完了したらその値ももらいたいはずです。
このときに登場するのがTask
型です。
Task
オブジェクトはこれらの状態と戻り値を教えてくれます。
public int RunTaskA()
{
Task taskB = RunTaskBAsync(); // Task型のオブジェクトを返す
bool status1 = taskB.IsCompletedSuccessfully; // 正常終了したかどうか
bool status2 = taskB.IsFaulted; // 例外が発生して異常終了したかどうか
bool status3 = taskB.IsCanceled; // 処理が途中でキャンセルされたかどうか
var returnValue = taskB.Result; // 戻り値(ある場合)
}
実際はこのように状態を取得してハンドリングするようなコードをゴリゴリ書くことはしません。
では、タスクBが完了した後にタスクAの続きを実行するためにはどのように記述すればよいでしょうか?そこで登場するのがasync/await
です。
await
非同期処理しているタスクの完了を待つ場合はawait
を使います。
await RunTaskBAsync(); // タスクBに戻り値がない(void)場合
var returnValue = await RunTaskBAsync(); // タスクBに戻り値がある場合
タスクBが正常終了した後、その戻り値を使って続きの処理をする場合は下記のようになります。
var result = await RunTaskBAsync();
Console.WriteLine(result); // 続きの処理
タスクBが異常終了、またはキャンセルされた場合のエラーハンドリングはtry/catch
を使って下記のようになります。
try
{
await RunTaskBAsync();
}
catch (TaskCanceledException ex)
{
// キャンセルされた場合の例外処理
}
catch (Exception ex)
{
// 異常終了した場合の例外処理
}
なお、await
はTask
型に対して使用します。そのため、必ずしもメソッドの実行時にawait
を使う必要はなく、待機のタイミングを遅らせることもできます。
Task<int> taskB = RunTaskBAsync(); // awaitをつけていないのでTask<int>型を受け取る
// 何か別の処理 (この間もタスクBは非同期で実行されている)
int result = await taskB; // ここでタスクBの完了を待つ
非同期処理っぽさが出てきましたね。
async
await
を使う場合、下記ルールに則って実装します。
- メソッドに
async
キーワードを付与 (文法) - 戻り型として
Task
型を利用[2][3] (文法)- returnする場合:
Task<戻り値の型>
- returnしない場合:
Task
- returnする場合:
- メソッド名の語尾にAsyncをつける (推奨)
- 呼び出す側が非同期処理であることを把握しやすくするため
public async Task<int> RunTaskAAsync()
{
int result = await RunTaskBAsync();
return result + 1; // taskBの戻り値を使った計算例
}
このサンプルコードでのポイントは「戻り型はTask<int>
型なのに、実際にreturnしているのはint
型である」という点です。実は async
キーワードをつけると、戻り値をTask
型で自動的にラップして返してくれるようになります。これで呼び出し元はTask
型のオブジェクトを使って非同期処理のハンドリングができますし、自分でTask
型を生成しなくていいので楽ですね。
以上のポイントを踏まえ、 タスクA -> タスクB -> タスクC の非同期呼び出しを実装すると下記になります。
public async Task<int> RunTaskAAsync()
{
var result = await RunTaskBAsync(); // taskBの完了を待つ
return result + 1;
}
private async Task<int> RunTaskBAsync()
{
await RunTaskCAsync(); // taskCの完了を待つ
return 1 + 2 + 3;
}
private async Task RunTaskCAsync()
{
await Task.Delay(500); // 0.5秒待機
}
同時に複数の非同期処理を扱う
例えば、タスクBとタスクCを同時に処理して、両方が完了した後にタスクAの続きを処理したい場合があります。
そういった場合はTask.WhenAll
を使用します。Task.WhenAll
は、「複数のタスクが全て完了したかどうかを確認できるTask
」を返します。Task
型なのでawait
できます。
public async Task<int> RunTaskAAsync()
{
Task<int> taskB = RunTaskBAsync();
Task<int> taskC = RunTaskCAsync();
int[] results = await Task.WhenAll(taskB, taskC); // 両方完了後、戻り値が配列として返る
return results[0] + results[1]; // タスクBとタスクCの結果を加算
}
private async Task<int> RunTaskBAsync()
{
await Task.Delay(100); // 0.1秒待機
return 1 + 2 + 3;
}
private async Task<int> RunTaskCAsync()
{
await Task.Delay(200); // 0.2秒待機
return 4 + 5 + 6;
}
まとめ
完全に理解した。
続きも書きました 👉 async/await を完全に理解してからもう少し理解する
-
UI操作に関する
async void
パターンもありますがここでは敢えて触れません。初心者は気にしないように。 ↩︎
Discussion