【C#】非同期処理の威力をさっくり体感する【async|await】

# はじめに
「非同期処理って体感的にわかるくらい変わるんスカ？」と先生に聞いたところ、以下のAPIに大量のリクエスト送ってみろと言われたのでやってみました。
UIを止めないとかはわかりやすいんですが、サーバー側では理論はわかっても体感がし辛いので記事にします。

## 環境
Mac
dotnet8

# サンプルコード
以下のようなエンドポイントを作ります。
どちらも0.1秒待機して、ただ数字を返す簡単なAPIです。
違いは同期か非同期かだけ。

```csharp
[ApiController]
[Route("api/[controller]")]
public class SampleController(ILogger<SampleController> logger) : ControllerBase
{
    [HttpGet("sync")]
    public int Index1()
    {
        logger.LogInformation("Index1 endpoint was called.");
        Thread.Sleep(100);
        return 1;
    }

    [HttpGet("async")]
    public async Task<int> Index2()
    {
        logger.LogInformation("Index2 endpoint was called.");
        await Task.Delay(100);
        return 1;
    }
}
```

## jmeterでレスポンスタイムを計測
シナリオは省きますが、5000リクエストを4回それぞれぶん投げてみます。

![スクリーンショット 2024-07-15 3.21.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/855584/5a36a680-4525-d837-4001-0d871f231190.png)

## 結果

api/sample/sync 同期 
| Label   | # Samples | Average | Min | Max  | Std. Dev. | Error % | Throughput | Received KB/sec | Sent KB/sec | Avg. Bytes |
|---------|-----------|---------|-----|------|-----------|---------|------------|-----------------|-------------|------------|
| api/sync | 20000     | 3926    | 0   | 18058 | 2461.24   | 0.190%  | 17.80322   | 2.85            | 2.27        | 163.6      |

---

api/sample/async 非同期
| Label    | # Samples | Average | Min | Max | Std. Dev.          | Error % | Throughput       | Received KB/sec | Sent KB/sec     | Avg. Bytes |
|----------|-----------|---------|-----|-----|--------------------|---------|------------------|-----------------|-----------------|------------|
| api/async | 20000     | 100     | 100 | 140 | 2.1423981679412476 | 0.0     | 17.984447050190997 | 2.792506915019891 | 2.318307627563683 | 159.0      |

---

それぞれ見やすくすると以下のような感じになります。
| API       | 平均時間 (ms) | 最小時間 (ms) | 最大時間 (ms) |
|-----------|---------------|---------------|---------------|
| api/sync  | 3926          | 0             | 18058         |
| api/async | 100           | 100           | 140           |

同期の方は非同期の40倍近く時間がかかっています。
非同期の方はほぼ理論値と言っていいでしょう。（同期の最小時間0秒はよくわからない）
はえーすっごい違う。

## 図解
例えばスレッドが3つ立ち上がった状態とします。

同期処理は以下のようにリクエストがあった時点でスレッドがブロックされるので、スレッド毎にキッチリ1ミリ秒のロックがかかります。
| 時刻 (ms) | スレッド1      | スレッド2      | スレッド3      |
|-----------|----------------|----------------|----------------|
| 0         | リクエスト1開始 | リクエスト2開始 | リクエスト3開始 |
| 1-1000    | ブロック中      | ブロック中      | ブロック中      |
| 1000      | リクエスト1完了 | リクエスト2完了 | リクエスト3完了 |
| 1001      | リクエスト4開始 | リクエスト5開始 | リクエスト6開始 |

非同期処理なら以下のように待機中に別のタスクを挟めるので、ギチギチに処理が積まれていくイメージです。
| 時刻 (ms) | スレッド1           | スレッド2           | スレッド3           |
|-----------|---------------------|---------------------|---------------------|
| 0         | リクエスト1開始     | リクエスト2開始     | リクエスト3開始     |
| 1-1000    | リクエスト4~n開始 | リクエスト5~n+1開始 | リクエスト6~n+2開始 |
| 1000      | リクエスト1完了     | リクエスト2完了     | リクエスト3完了     |
| 1001~n      | リクエスト4~n完了    | リクエスト5~n+1完了    | リクエスト6~n+2完了    |

# まとめ
外部APIを叩く処理や、DBとの通信のような待機時間が発生する部分では非常に威力を発揮しそうです。
というかこの結果を見る限り、サーバーサイドでそれらを実装する上ではマストな要件な気がします。
なんでもかんでも非同期で実装すると痛い目を見ますが（実際に見た）IO処理が多発する実装をするときは、「とりあえず同期で実装する」のではなく、「とりあえず非同期で実装してみる」という頭の転換が必要そうです。