bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] async/await による非同期処理を理解する

[C#] async/await による非同期処理を理解する

Jun 29, 2026 C# , .NET bucket-sort

前回は、Task、Parallel、PLINQ を使った並列処理を見ました。

今回は、現代の C# で非常によく使う async/await パターンを整理します。

async/await は、処理を別スレッドで実行するためだけの機能ではありません。
主な目的は、I/O 待ちのような「時間はかかるが CPU を使い続けない処理」を、スレッドをブロックせずに待てるようにすることです。

async と await の基本

async は、そのメソッドの中で await を使えるようにする修飾子です。
await は、Task などの非同期処理の完了を待ちます。

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

using HttpClient client = new HttpClient();

string html = await client.GetStringAsync("https://example.com");

Console.WriteLine(html.Length);

await している間、現在のスレッドを占有して待ち続けるわけではありません。
I/O が完了したあと、続きの処理が再開されます。

非同期メソッドを書く

非同期メソッドは、通常 Task または Task<T> を返します。

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

static async Task<int> GetContentLengthAsync(string url)
{
    using HttpClient client = new HttpClient();
    string content = await client.GetStringAsync(url);
    return content.Length;
}

int length = await GetContentLengthAsync("https://example.com");
Console.WriteLine(length);

戻り値がある場合は Task<T>、戻り値がない場合は Task を返します。

static async Task DelayAndPrintAsync()
{
    await Task.Delay(1000);
    Console.WriteLine("完了しました。");
}

async void は基本的に避ける

戻り値がない非同期メソッドでも、基本的には Task を返します。

static async Task SaveAsync()
{
    await Task.Delay(500);
    Console.WriteLine("保存しました。");
}

async void は、主にイベントハンドラー用です。

private async void SaveButton_Click(object sender, EventArgs e)
{
    await SaveAsync();
}

async void は呼び出し側が完了や例外を追跡しづらいため、通常のメソッドでは避けます。

SynchronizationContext と async/await

await の後、どのスレッドで処理が再開されるかは実行環境によって変わります。

WPF や Windows Forms のような UI アプリには、UI スレッドへ戻るための SynchronizationContext があります。
ASP.NET やコンソールアプリでは、環境によって挙動が異なります。

UI アプリでは、次のようなコードが自然に書けます。

private async void LoadButton_Click(object sender, EventArgs e)
{
    StatusLabel.Text = "読み込み中...";

    string text = await LoadTextAsync();

    StatusLabel.Text = text;
}

await の後に UI スレッドへ戻るため、UI コントロールを更新できます。

ConfigureAwait の役割

ライブラリコードでは、ConfigureAwait(false) を使うことがあります。

static async Task<string> LoadTextAsync(HttpClient client)
{
    string text = await client
        .GetStringAsync("https://example.com")
        .ConfigureAwait(false);

    return text;
}

ConfigureAwait(false) は、「await 後に元のコンテキストへ戻らなくてもよい」と伝えます。

UI 更新が必要なアプリケーションコードでは、むやみに付けると UI スレッドへ戻らず困ることがあります。
一方、UI に依存しないライブラリコードでは、不要なコンテキスト復帰を避けられるため有用です。

非同期メソッドの命名規約

非同期メソッドには、末尾に Async を付けるのが一般的です。

Task SaveAsync()
Task<string> LoadTextAsync()
Task<int> CountItemsAsync()

Async サフィックスがあると、呼び出し側は await すべきメソッドだと判断しやすくなります。

例外として、イベントハンドラーやインターフェイス制約など、命名を自由に変えられない場合もあります。

複数の await を使う

非同期メソッド内では、複数の await を順番に書けます。

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

static async Task PrintTwoPagesAsync()
{
    using HttpClient client = new HttpClient();

    string first = await client.GetStringAsync("https://example.com");
    string second = await client.GetStringAsync("https://example.org");

    Console.WriteLine(first.Length);
    Console.WriteLine(second.Length);
}

このコードでは、1 つ目が終わってから 2 つ目を開始します。

独立した処理を同時に始めたい場合は、先に Task を作ってから await Task.WhenAll() します。

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

static async Task PrintTwoPagesInParallelAsync()
{
    using HttpClient client = new HttpClient();

    Task<string> firstTask = client.GetStringAsync("https://example.com");
    Task<string> secondTask = client.GetStringAsync("https://example.org");

    string[] results = await Task.WhenAll(firstTask, secondTask);

    Console.WriteLine(results[0].Length);
    Console.WriteLine(results[1].Length);
}

await は「待つ」構文なので、どこで Task を開始するかが重要です。

同期メソッドから非同期メソッドを呼ぶ

非同期メソッドは、できるだけ呼び出し元まで async を伝播させるのが基本です。

static async Task Main()
{
    await SaveAsync();
}

どうしても同期メソッドから呼ぶ場合、Result や Wait() を使うとブロックします。

SaveAsync().GetAwaiter().GetResult();

これは例外の包み方が比較的扱いやすいものの、ブロックであることには変わりません。
UI アプリや ASP.NET ではデッドロックや応答性低下の原因になるため、可能な限り async all the way を意識します。

catch と finally で await する

C# では、catch や finally の中でも await を使えます。

static async Task SaveWithCleanupAsync()
{
    try
    {
        await SaveAsync();
    }
    catch (Exception ex)
    {
        await LogAsync(ex);
        throw;
    }
    finally
    {
        await CleanupAsync();
    }
}

エラーログ送信や一時ファイル削除が非同期 API の場合でも、自然に後始末を書けます。

ValueTask を返す非同期メソッド

Task<T> の代わりに ValueTask<T> を返すメソッドもあります。

using System.Threading.Tasks;

static ValueTask<int> GetCachedValueAsync(bool cached)
{
    if (cached)
    {
        return ValueTask.FromResult(42);
    }

    return new ValueTask<int>(LoadValueAsync());
}

static async Task<int> LoadValueAsync()
{
    await Task.Delay(500);
    return 42;
}

ValueTask<T> は、同期的に結果を返せることが多い高性能な API で使われます。
通常のアプリケーションコードでは、まず Task / Task<T> を使えば十分です。

ローカル関数と async/await

ローカル関数にも async を付けられます。

using System;
using System.Threading.Tasks;

static async Task RunAsync()
{
    await PrintAfterDelayAsync("A");
    await PrintAfterDelayAsync("B");

    static async Task PrintAfterDelayAsync(string message)
    {
        await Task.Delay(500);
        Console.WriteLine(message);
    }
}

メソッド内だけで使う小さな非同期処理を切り出すと、処理の流れが読みやすくなります。

async/await のキャンセル

非同期処理でも、キャンセルには CancellationToken を使います。

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

static async Task<string> DownloadAsync(string url, CancellationToken token)
{
    using HttpClient client = new HttpClient();
    return await client.GetStringAsync(url, token);
}

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));

try
{
    string text = await DownloadAsync("https://example.com", cts.Token);
    Console.WriteLine(text.Length);
}
catch (OperationCanceledException)
{
    Console.WriteLine("キャンセルされました。");
}

キャンセル対応 API には、できるだけ CancellationToken を渡します。
独自の非同期メソッドでも、長い処理では token を受け取る設計にしておくと呼び出し側が制御しやすくなります。

非同期ストリーム

IAsyncEnumerable<T> を使うと、非同期に値を 1 つずつ返せます。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

static async IAsyncEnumerable<int> GenerateNumbersAsync()
{
    for (int i = 1; i <= 5; i++)
    {
        await Task.Delay(300);
        yield return i;
    }
}

await foreach (int number in GenerateNumbersAsync())
{
    Console.WriteLine(number);
}

大量データを一括で返すのではなく、準備できたものから順に流したい場合に便利です。
API のページング、ログの読み取り、ストリーミング処理などと相性がよいです。

Parallel.ForEachAsync

.NET 6 以降では、Parallel.ForEachAsync を使って非同期処理を並列実行できます。

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

string[] urls =
{
    "https://example.com",
    "https://example.org",
    "https://example.net"
};

using HttpClient client = new HttpClient();

await Parallel.ForEachAsync(
    urls,
    new ParallelOptions { MaxDegreeOfParallelism = 2 },
    async (url, token) =>
    {
        string text = await client.GetStringAsync(url, token);
        Console.WriteLine($"{url}: {text.Length}");
    });

Parallel.ForEach は同期処理の並列化に向いています。
Parallel.ForEachAsync は、HTTP リクエストのような非同期処理を複数同時に進めたい場合に向いています。

まとめ

async/await は、非同期処理を読みやすく書くための C# の中心機能です。

Task や Task<T> は非同期処理の結果を表し、await は完了を自然に待つための構文です。
UI アプリでは SynchronizationContext、ライブラリコードでは ConfigureAwait(false)、長時間処理では CancellationToken を意識すると、実務で扱いやすいコードになります。

また、非同期ストリームや Parallel.ForEachAsync を使うことで、現代的な I/O 中心の処理も自然に表現できます。

C# .NET Async Await Task ValueTask IAsyncEnumerable Parallel.ForEachAsync 非同期処理
← [C#] Task Parallel Library と PLINQ を理解する [C#] 名前空間とアセンブリの基本を理解する →

Related Posts

  • [C#] async/await の振る舞いと使い方 Mar 20, 2026
  • [C#] Task Parallel Library と PLINQ を理解する Jun 28, 2026
  • [C#] Interlockedの使い方 Mar 27, 2026
  • [C#] 非同期処理における競合対策 Mar 21, 2026

Table of Contents

  • async と await の基本
  • 非同期メソッドを書く
  • async void は基本的に避ける
  • SynchronizationContext と async/await
  • ConfigureAwait の役割
  • 非同期メソッドの命名規約
  • 複数の await を使う
  • 同期メソッドから非同期メソッドを呼ぶ
  • catch と finally で await する
  • ValueTask を返す非同期メソッド
  • ローカル関数と async/await
  • async/await のキャンセル
  • 非同期ストリーム
  • Parallel.ForEachAsync
  • まとめ

Recent Posts

  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 属性でコードに意味を与える Jul 8, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] リフレクションで型の中身を調べる Jul 6, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Categories

  • C#120
  • .NET119
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • System.Collections.Generic
  • デリゲート
  • Code Snippet
  • DynamoDB
  • LINQ
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メモリ管理
  • リフレクション
  • DoS
  • Nginx
  • WordPress
  • メタデータ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • インターフェース
  • オブジェクト指向
  • クラス設計
  • デザインパターン
  • パターンマッチング
  • ポインター
  • 継承
Powered by Hugo & Explore Theme.