async/await を完全に理解してからもう少し理解する
この記事は「async/await を完全に理解する」の続きになります。
この記事では「完全に理解した」先の「なにもわからない」「チョットデキル」レベル[1]を目指すための補足説明をします。
なお、前回の記事より少し踏み込んだ内容になるためC#色が強い内容になりますが、同様の仕組みは他言語にもあるはずなので多少参考にはなるかと思います。
既存の同期処理を非同期化する
既存の同期メソッドを、呼び出し側から非同期扱いすることができます。
Task.Run
を使うと処理をTask
でラップすることができます。
public async Task<int> RunTaskAAsync()
{
var result = await Task.Run(RunTaskB); // Task<int>化されるのでawaitできる
return result + 1;
}
private int RunTaskB() // 同期メソッド
{
return 1 + 2 + 3;
}
非同期処理の完了を待たない
非同期処理は必ずしも待つ必要はありません。投げっぱなしにしたいケースがあります。
そういった場合は単純に待たなければ良い、つまりawait
を使わなければ良いです。
public int RunTaskA()
{
RunTaskBAsync(); // awaitを使わない
var result = 1 + 2 + 3; // タスクBの完了を待たずに処理を続ける
return result; // タスクBの完了を待たずにreturnする
}
この場合のポイントは下記になります。
- タスクBの完了を待たずに処理を続けることができる
- タスクBが失敗したとしてもタスクAには影響はない
ちなみにこのように実装するとVisual Studio審判長から以下のようなイエローカードが提示されます。
この呼び出しを待たないため、現在のメソッドの実行は、呼び出しが完了するまで続行します。呼び出しの結果に 'await' 演算子を適用することを検討してください。
わかっていて敢えてawait
をつけていないことを審判に示すためには下記のように実装します。
var _ = RunTaskBAsync(); // taskを利用しないことを明示
非同期メソッドを同期メソッドから呼び出す
非同期処理をする場合、基本的にはasync/await
パターンが推奨されるのですが、既存実装の改修などでは戻り値をTask
型に変更できないケースも多いです。
そういった場合はこれらを利用します。
-
Task
型:task.Wait
メソッド -
Task<T>
型:task.Result
プロパティ
public int RunTaskA()
{
int result = RunTaskBAsync().Result; // 完了の待機と戻り値の取得
return result + 1;
}
async
を使う必要がないケース
非同期メソッドを実装するときには必ずしもasync
を使う必要はありません。前回の記事で説明したように、async
キーワードは自動的にTask
を生成してくれますが、逆に言えば自力でTask
オブジェクトをreturnすることができればasync
に頼る必要はありません。そのようなケースは主に2つあります。
他の非同期メソッドの戻り値をそのままreturnできる場合
例えばタスクAがタスクBを呼び出し、その結果をそのままreturnする場合
public async Task<int> RunTaskAAsync()
{
return await RunTaskBAsync(); // int型をreturnしてasyncにラップしてもらう
}
private async Task<int> RunTaskBAsync()
{
await Task.Delay(1000); // 1秒待機
return 1 + 2 + 3;
}
この場合、async/await
を使わずにこのように記載することもできます。
public Task<int> RunTaskAAsync()
{
return RunTaskBAsync(); // Task<int>型をreturnする
}
private async Task<int> RunTaskBAsync()
{
await Task.Delay(1000); // 1秒待機
return 1 + 2 + 3;
}
後者のように書くメリットは、前者だとawait
でTask
を剥がした後にasync
でまたTask
にラップされるので、その余計なステップを排除できることでしょうか(そこまで気にしなくていい気もするけど)。私はなんとなく後者で書ける場合はそのように書くようにしています。
Task
を生成する場合
自力でTask
を自力で生成する場合の代表例は、前述した既存同期メソッドの非同期化です。
public Task<int> RunTaskAAsync()
{
Task<int> taskB = Task.Run(RunTaskB);
return taskB;
}
private int RunTaskB() // 同期メソッド
{
return 1 + 2 + 3;
}
他には、「戻り型はTask
型だが処理は非同期である必要がない」という場合があります。具体的には下記のような場合です。
- 元々は非同期メソッドだったものが改修により非同期である必要がなくなった
- interfaceの戻り型の定義が
Task
になっているが、それを実装した際に非同期処理がなかった
こういった場合にはTask.CompletedTask
でTask
型のオブジェクト、Task.FromResult
でTask<T>
型のオブジェクトを生成できます。
public Task RunTask1Async()
{
// 同期処理
return Task.CompletedTask; // "正常終了"を表すTaskオブジェクト
}
public Task<int> RunTask2Async()
{
// 同期的な計算処理
var result = 1 + 2 + 3;
return Task.FromResult(result); // 値をTaskでラップして返す
}
非同期処理のキャンセル
あるタスクを非同期実行した後、そのタスクをキャンセルしたい場合があります。例えば「サーバーに通信したはいいものの応答が遅すぎるので通信を切りたい」など。
そういった場合はCancellationToken
を使います。CancellationToken
は呼び出し元から非同期メソッドにキャンセル依頼をするためのものです。これは事前に呼び出し元で生成して非同期メソッドに渡しておく必要があります。
var cts = new CancellationTokenSource();
var taskB = RunTaskBAsync(cts.Token); // CancellationTokenを渡す
// なんらかの処理
cts.Cancel(); // タスクBにキャンセルを依頼
ただし、このキャンセルの仕組みを利用するためには当然ですが非同期メソッド側が下記のようにキャンセルに対応していないといけません。自作する場合には気をつけましょう。
- 引数で
CancellationToken
を受け取れるようになっている -
CancellationToken
を監視して、キャンセル依頼が来たときに反応できるようになっている - キャンセル処理をして
TaskCanceledException
をthrowできるようになっている
まとめ
僕、チョットデキル?
前回の記事はこちら 👉 async/await を完全に理解する
Discussion