async/await を完全に理解してからもう少し理解する

公開:2020/10/04
更新:2020/10/06
4 min読了の目安(約4100字TECH技術記事

この記事は「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;
}

task.Wait task.Resultを使うとデッドロックが発生する可能性があります。どうしてもという場合のみ気をつけて使いましょう。
参考: async/awaitについての備忘録 - async/await, Taskのタブー②

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;
}

後者のように書くメリットは、前者だとawaitTaskを剥がした後に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.CompletedTaskTask型のオブジェクト、Task.FromResultTask<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 を完全に理解する

脚注
  1. cf. パソコンの大先生によるエンジニア用語解説 ↩︎