C# Tips | 非同期・並列処理:async/awaitを使う

C# C#
スポンサーリンク
スポンサーリンク

async/awaitとは「待っている間にスレッドを解放する」ための仕組みです

業務・実務でC#を使っている読者のみなさんにとって、 非同期処理はもはや避けて通れないテーマです。

  • Web APIで外部サービスを呼び出す
  • データベースに問い合わせる
  • ファイルを読み書きする
  • ネットワーク越しに大量データを送受信する

こうした処理は「時間がかかる処理」です。 もしすべてを同期的(普通のメソッド)に書いてしまうと、

  • UIが固まる
  • サーバーのスレッドが待ちっぱなしになる
  • 同時接続数が増えると一気にパフォーマンスが落ちる

といった問題が起きます。

async/await は、 「時間のかかる処理を待っている間、スレッドを解放しておき、 終わったら続きを再開する」ための仕組みです。

ここから、プログラミング初心者の方にも分かるように、 ステップバイステップで async/await の考え方と使い方を解説し、 業務で使えるテンプレートや注意点まで丁寧に掘り下げていきます。

ステップ1:まず「同期処理」と「非同期処理」の違いをイメージする

同期処理のイメージ

同期処理は「終わるまで待つ」スタイルです。

public void DoWork()
{
    Console.WriteLine("処理開始");

    // 3秒かかる処理(例:外部API呼び出し)
    System.Threading.Thread.Sleep(3000);

    Console.WriteLine("処理終了");
}
C#

このメソッドを呼ぶと、

  • 呼び出し元は3秒間、何もできません
  • UIスレッドで呼ぶと、画面が3秒固まります

という状態になります。

非同期処理のイメージ

非同期処理は「待っている間に他のことをしてよい」スタイルです。

public async Task DoWorkAsync()
{
    Console.WriteLine("処理開始");

    // 3秒かかる処理を非同期で待つ
    await Task.Delay(3000);

    Console.WriteLine("処理終了");
}
C#

このメソッドを呼ぶと、

  • await Task.Delay(3000) の間、スレッドは解放されます
  • UIスレッドなら、画面は固まりません
  • サーバーなら、同じスレッドで他のリクエストを処理できます

という状態になります。

ここで重要なのは、

「非同期処理=並列処理」ではなく、 「非同期処理=待ち時間にスレッドを解放する」

というイメージです。

ステップ2:asyncメソッドの基本形を理解する

asyncメソッドの3つのポイント

async を使ったメソッドには、基本的に次の3つの特徴があります。

  1. 戻り値が Task または Task<T> になる
  2. メソッド宣言に async キーワードを付ける
  3. メソッド内で await を使って非同期処理を待つ

最もシンプルな例を見てみます。

public async Task SimpleAsync()
{
    Console.WriteLine("開始");
    await Task.Delay(1000); // 1秒待つ(非同期)
    Console.WriteLine("終了");
}
C#

呼び出し側:

await SimpleAsync();
C#

ここでのポイントは次のとおりです。

  • SimpleAsync は「非同期に動くメソッド」であり、戻り値は Task です。
  • await を使うことで、「ここで一旦中断し、処理が終わったら再開する」という動きになります。
  • 呼び出し側も await を使うため、呼び出し側のメソッドも async になります(これが「asyncの伝染」と呼ばれることがあります)。

ステップ3:戻り値付きのasyncメソッド(Task<T>)を使う

非同期に値を返すメソッドの例

外部APIから文字列を取得するようなイメージで考えてみます。

public async Task<string> GetMessageAsync()
{
    // 実際にはここでHTTPクライアントなどを使います
    await Task.Delay(500); // 疑似的な待ち時間

    return "Hello from async!";
}
C#

呼び出し側:

string message = await GetMessageAsync();
Console.WriteLine(message);
C#

ここでのポイントは次のとおりです。

  • 戻り値付きの非同期メソッドは Task<T> を返します。
  • await すると、Task<T> の中身(T)が取り出されます。
  • 呼び出し側は「普通の戻り値を受け取る感覚」で書けますが、内部では非同期処理が行われています。

ステップ4:実務でよくあるパターン「I/O待ちをasync/awaitに置き換える」

ファイル読み込みを同期で書いた場合

using System.IO;

public string ReadFileSync(string path)
{
    return File.ReadAllText(path); // 同期的に読み込む
}
C#

これは、

  • 大きなファイルを読むときにスレッドが長時間占有される
  • サーバー側で大量アクセスがあると、スレッド枯渇の原因になる

という問題があります。

ファイル読み込みを非同期で書き直す

using System.IO;
using System.Threading.Tasks;

public async Task<string> ReadFileAsync(string path)
{
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync(); // 非同期読み込み
}
C#

呼び出し側:

string content = await ReadFileAsync("data.txt");
Console.WriteLine(content);
C#

ここでのポイントは次のとおりです。

  • ReadToEndAsync は非同期メソッドであり、I/O待ちの間にスレッドを解放します。
  • await を使うことで、読み込み完了後に自然な形で処理を再開できます。
  • サーバー側では、同じスレッドで他のリクエストを処理できるため、スケーラビリティが向上します。

同じように、

  • HTTPクライアント(HttpClient.GetAsync/SendAsync)
  • データベースアクセス(EF Coreの ToListAsync など)
  • ネットワークI/O全般

は、「同期版ではなく非同期版を使う」のが実務では基本になります。

ステップ5:async/awaitでよくある落とし穴と、その回避方法(重要な深掘り)

1. async void は基本的に使わない

async メソッドは、通常 Task または Task<T> を返しますが、 C#では async void も書けてしまいます。

public async void DoSomethingAsync()
{
    await Task.Delay(1000);
    // ...
}
C#

しかし、async void は次の理由で危険です。

  • 例外が呼び出し側に伝わらない(ハンドリングが難しい)
  • 呼び出し側から「待つ」ことができない(await できない)
  • テストしづらい

そのため、

  • イベントハンドラ(button.Click += async (s, e) => ...)など、 「戻り値を持てない場面」以外では async void を使わない

というのが実務での鉄則です。

2. Task.Run を乱用しない

Task.Run は「別スレッドで処理を実行する」ためのメソッドですが、 I/O待ちを非同期にしたいだけなら、基本的に Task.Run は不要です。

悪い例:

public async Task<string> GetDataBadAsync()
{
    return await Task.Run(() =>
    {
        // 実際にはHTTP呼び出しなどのI/O処理
        System.Threading.Thread.Sleep(1000);
        return "data";
    });
}
C#

これは、

  • スレッドを余計に消費する
  • I/O待ちなのにCPUスレッドを占有してしまう

という意味で非効率です。

良いパターンは、

  • I/O処理はライブラリの非同期メソッド(ReadAsync/SendAsyncなど)を使う
  • CPU負荷の高い計算だけを Task.Run で別スレッドに逃がす

という使い分けです。

3. Result や Wait() で同期的に待たない

Task には Result プロパティや Wait() メソッドがありますが、 これを使って同期的に待つと、デッドロックの原因になります。

悪い例:

public string GetMessageSync()
{
    return GetMessageAsync().Result; // 同期的に待つ
}
C#

UIスレッドやASP.NETの古い同期コンテキストでは、 これがデッドロックを引き起こすことがあります。

基本方針はシンプルです。

「非同期メソッドは、非同期のまま await でつなぐ」 「Result や Wait() で無理やり同期に戻さない」

ステップ6:業務で使える「async/awaitユーティリティ」のイメージ

共通の「非同期ラッパー」を用意する

業務コードの中で、 「時間のかかる処理」をラップするユーティリティを用意しておくと便利です。

using System;
using System.Threading.Tasks;

public static class AsyncUtility
{
    public static async Task ExecuteWithRetryAsync(
        Func<Task> action,
        int maxRetryCount,
        TimeSpan delayBetweenRetries)
    {
        for (int i = 0; i < maxRetryCount; i++)
        {
            try
            {
                await action();
                return;
            }
            catch (Exception ex)
            {
                if (i == maxRetryCount - 1)
                {
                    // ログ出力などを行ったうえで再スローするイメージ
                    Console.WriteLine($"リトライ失敗: {ex.Message}");
                    throw;
                }

                await Task.Delay(delayBetweenRetries);
            }
        }
    }

    public static async Task<T> ExecuteWithRetryAsync<T>(
        Func<Task<T>> action,
        int maxRetryCount,
        TimeSpan delayBetweenRetries)
    {
        for (int i = 0; i < maxRetryCount; i++)
        {
            try
            {
                return await action();
            }
            catch (Exception ex)
            {
                if (i == maxRetryCount - 1)
                {
                    Console.WriteLine($"リトライ失敗: {ex.Message}");
                    throw;
                }

                await Task.Delay(delayBetweenRetries);
            }
        }

        throw new InvalidOperationException("リトライロジックに到達してはいけない場所です。");
    }
}
C#

利用例:外部API呼び出しを非同期+リトライでラップする

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class ExternalApiClient
{
    private readonly HttpClient _httpClient = new HttpClient();

    public async Task<string> GetDataAsync()
    {
        var response = await _httpClient.GetAsync("https://example.com/api/data");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }

    public async Task<string> GetDataWithRetryAsync()
    {
        return await AsyncUtility.ExecuteWithRetryAsync(
            async () => await GetDataAsync(),
            maxRetryCount: 3,
            delayBetweenRetries: TimeSpan.FromSeconds(2));
    }
}
C#

ここでのポイントは次のとおりです。

  • 非同期処理(外部API呼び出し)を async/await で書く
  • 共通ユーティリティでリトライロジックをラップする
  • 呼び出し側は「非同期メソッドを呼ぶだけ」で、リトライや待ち時間の詳細を意識しなくてよい

こうしたユーティリティをプロジェクトに用意しておくと、 「非同期処理+エラーハンドリング+リトライ」を一貫した形で扱えるようになります。

ステップ7:async/awaitを業務で使うときのセキュリティ・品質面の注意点

1. 例外処理を必ず意識する

非同期メソッドでも、例外は普通に発生します。

public async Task<string> GetDataAsync()
{
    var response = await _httpClient.GetAsync("https://example.com/api/data");
    response.EnsureSuccessStatusCode(); // ここで例外が出ることがある
    return await response.Content.ReadAsStringAsync();
}
C#

呼び出し側では、

try
{
    string data = await client.GetDataAsync();
}
catch (HttpRequestException ex)
{
    // ログ出力・ユーザーへのメッセージなど
}
C#

のように、await の周りで例外処理を書くことが重要です。

2. キャンセル(CancellationToken)を組み込む

業務システムでは、

  • ユーザーが操作をキャンセルした
  • タイムアウトを超えた
  • サーバー側でリクエストを打ち切りたい

といった場面があります。

async/await と組み合わせて、 CancellationToken を使うことで、 「途中でやめる」ことができます。

簡単なイメージ:

public async Task<string> GetDataAsync(CancellationToken cancellationToken)
{
    var response = await _httpClient.GetAsync("https://example.com/api/data", cancellationToken);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync(cancellationToken);
}
C#

キャンセルを正しく扱うことで、

  • 無駄な処理を減らす
  • リソース消費を抑える
  • ユーザー体験を向上させる

といった効果があります。

まとめ:async/awaitは「待ち時間を味方にして、スレッドを賢く使うための標準パターン」です

async/await を使うC#ユーティリティの本質は次の3つです。

  • 時間のかかる処理(I/O・外部API・DBアクセスなど)を、同期的に「待ちっぱなし」にするのではなく、async/await を使って待ち時間にスレッドを解放し、終わったら自然な形で処理を再開できるようにすること。
  • Task/Task<T> を戻り値とする async メソッドを基本とし、await でつないでいくことで、非同期処理でありながら「ほぼ同期処理と同じ書き心地」で業務ロジックを書けるようにすること。
  • 例外処理・リトライ・キャンセル・async void 禁止・Result/Wait 回避といった実務上の注意点を意識しながら、共通ユーティリティ(AsyncUtilityなど)を用意して、プロジェクト全体で非同期処理の品質と一貫性を高めること。

ここで紹介したステップとテンプレートをベースにすることで、 読者のみなさんは業務システムの中に 「async/awaitを軸にした、読みやすくてスケーラブルな非同期処理レイヤー」を 自然に組み込めるようになります。

タイトルとURLをコピーしました