【C#】async/await完全ガイド|ステートマシン・ConfigureAwait・ValueTask・IAsyncEnumerable・デッドロック対策まで

C# の async/await は「非同期処理を同期処理のように書ける」構文です。しかし「とりあえず動く」レベルを超えると、ConfigureAwait(false)ValueTask、デッドロック、async void の罠など、知らないと痛い目を見るトピックが次々と現れます。

本記事では基本構文からコンパイラが生成するステートマシンの仕組み、IAsyncEnumerable(非同期ストリーム)、IProgress<T>(進捗報告)、実践パターン(RetryAsync / TimeoutAsync)まで体系的に解説します。CancellationToken によるキャンセルは非同期タスクのキャンセル方法|CancellationTokenを、TaskParallel の使い分けは非同期処理と並列処理の違いを参照してください。

スポンサーリンク

async/await の基本構文

async 修飾子を付けたメソッドの中でのみ await が使えます。await は「この非同期操作が完了するまでここで一時停止し、スレッドは他の処理に使わせる」という意味です。

async/await の基本
using System.Net.Http;

// async メソッドは Task(値なし)か Task<T>(値あり)を返す
static async Task<string> FetchPageAsync(string url)
{
    using var client = new HttpClient();
    // await: 完了まで待機するが、スレッドはブロックしない
    string html = await client.GetStringAsync(url);
    return html;
}

static async Task Main()
{
    Console.WriteLine("取得開始");
    string content = await FetchPageAsync("https://example.com");
    Console.WriteLine($"取得完了: {content.Length} 文字");
}
await の本質
await は「待機中にスレッドを解放し、完了後に処理を再開する」という制御フローの中断・再開です。スレッドを占有し続ける同期的な .Wait()/.Result とは根本的に異なります。

コンパイラが生成するステートマシン

async メソッドはコンパイル時にステートマシン(状態機械)に変換されます。各 await 地点が「状態」になり、コンパイラが再開ロジックを自動生成します。

async メソッドとコンパイラ生成コード(概念)
// 書いたコード
static async Task<int> ComputeAsync()
{
    int a = await GetAAsync();   // ← 状態0 → 状態1
    int b = await GetBAsync();   // ← 状態1 → 状態2
    return a + b;
}

// コンパイラが生成する(概念的な擬似コード)
// 実際はより複雑な IAsyncStateMachine 実装になる
class ComputeAsync_StateMachine : IAsyncStateMachine
{
    private int _state = 0;
    private int _a, _b;
    private TaskAwaiter<int> _awaiter;
    public AsyncTaskMethodBuilder<int> _builder;

    public void MoveNext()
    {
        switch (_state)
        {
            case 0:
                _awaiter = GetAAsync().GetAwaiter();
                if (!_awaiter.IsCompleted)
                {
                    _state = 1;
                    _awaiter.OnCompleted(MoveNext);   // 完了後に MoveNext を再呼び出し
                    return;
                }
                goto case 1;
            case 1:
                _a = _awaiter.GetResult();
                _awaiter = GetBAsync().GetAwaiter();
                if (!_awaiter.IsCompleted)
                {
                    _state = 2;
                    _awaiter.OnCompleted(MoveNext);
                    return;
                }
                goto case 2;
            case 2:
                _b = _awaiter.GetResult();
                _builder.SetResult(_a + _b);         // Task を完了状態にする
                break;
        }
    }
}
ステートマシンで覚えておく3点
await の数だけ状態が増える
② 非同期操作が既に完了していれば OnCompleted(コールバック)を登録せず即続行する(ゼロコスト)
ValueTask はこの「既に完了」ケースを最適化するために存在する

async メソッドの戻り値の型

戻り値型 用途 使いどころ
Task 値を返さない非同期処理 一般的な非同期メソッド(void の代わり)
Task<T> 値を返す非同期処理 最も一般的。await で値を取り出せる
ValueTask 値を返さない・頻繁に同期完了する処理 ヒープアロケーションを削減したい高頻度メソッド
ValueTask<T> 値を返す・頻繁に同期完了する処理 キャッシュ返却・高スループット API
voidasync void イベントハンドラー専用 それ以外では使用禁止(後述)
IAsyncEnumerable<T> 非同期で連続して値を生成 await foreach で消費する非同期ストリーム

ValueTask vs Task — いつ使うか

ValueTask の適切な使い方
// Task: 常にヒープにオブジェクトが生成される
public async Task<int> AlwaysAllocatesAsync()
{
    await Task.Delay(10);
    return 42;
}

// ValueTask: キャッシュから返すとき(同期完了)はアロケーションゼロ
private int _cachedValue = -1;

public ValueTask<int> GetCachedOrFetchAsync()
{
    if (_cachedValue >= 0)
        return new ValueTask<int>(_cachedValue);  // アロケーションなし!

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

private async Task<int> FetchAndCacheAsync()
{
    await Task.Delay(100);  // 実際の非同期処理
    _cachedValue = 42;
    return _cachedValue;
}

// ValueTask の重要な制約: 1回しか await できない
// NG: ValueTask を複数回 await するとクラッシュする可能性がある
var vt = GetCachedOrFetchAsync();
var r1 = await vt;  // OK
// var r2 = await vt;  // 危険!もう一度 await してはいけない

// 複数箇所で使いたいなら .AsTask() で Task に変換する
var task = GetCachedOrFetchAsync().AsTask();
var r1b = await task;
var r2b = await task;  // Task は何度でも await できる

async void の罠

async void はイベントハンドラー以外では使ってはいけません。例外がキャッチできず、アプリがクラッシュします。

async void が危険な理由
// NG: async void は例外を呼び出し元に伝播できない
static async void DangerousAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("この例外はキャッチできない!");
}

static async Task Main()
{
    try
    {
        DangerousAsync();  // Fire and forget になる(await もできない)
        await Task.Delay(500);
        // 例外は SynchronizationContext に飛び、アプリが落ちる
    }
    catch (Exception ex)
    {
        // ここには到達しない!
        Console.WriteLine(ex.Message);
    }
}

// OK: async Task にすれば例外が await で伝播する
static async Task SafeAsync()
{
    await Task.Delay(100);
    throw new InvalidOperationException("この例外はキャッチできる");
}

static async Task MainSafe()
{
    try
    {
        await SafeAsync();   // await しているので例外が伝播
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);  // "この例外はキャッチできる"
    }
}

// 唯一の例外: イベントハンドラー(async Task にできない)
button.Click += async (sender, e) =>
{
    await DoSomethingAsync();  // イベントハンドラーのみ async void が許容される
};
async void チェックリスト
✗ 例外を呼び出し元に伝播できない
await できない(完了を待てない)
✗ テストが困難
button.Click += async (s, e) => { ... } のみ許容

SynchronizationContext と ConfigureAwait(false)

await が完了すると、デフォルトでは 元の SynchronizationContext(UIスレッド・ASP.NET リクエストコンテキスト等)に処理を戻そうとします。この「コンテキストへの復帰」がデッドロックの原因になることがあります。

ConfigureAwait(false) の使い方
// ライブラリコードの推奨パターン
public static async Task<string> ProcessDataAsync(string input)
{
    // ConfigureAwait(false): 完了後に元のコンテキストに戻らなくてよい
    // → スレッドプールのどのスレッドでも再開できる(高効率)
    var result = await ComputeAsync(input).ConfigureAwait(false);

    // ここは元の UI スレッドではなく、スレッドプールスレッドで実行される
    return result.ToUpper();
}

// アプリコード(WPF / MAUI)では ConfigureAwait(false) を付けない
private async void Button_Click(object sender, EventArgs e)
{
    // await 後に UI スレッドに戻る必要がある
    var data = await FetchDataAsync();   // ConfigureAwait(false) を付けない
    label.Text = data;                   // UI スレッドでないと失敗する
}
コード種別 ConfigureAwait(false) 理由
ライブラリ・共通処理 付ける 呼び出し元のコンテキストに依存しない。パフォーマンス向上
WPF / MAUI の UI 処理 付けない await 後に UI スレッドで続きを実行する必要がある
ASP.NET Core 付けても付けなくても OK .NET Core 以降は SynchronizationContext が存在しないため実質同じ
コンソールアプリ 付けても付けなくても OK SynchronizationContext なし

デッドロック — .Result / .Wait() の罠

非同期メソッドを同期的にブロックすると、クラシックなデッドロックが起きます。特に WPF や ASP.NET(非 Core)で頻発するパターンです。

デッドロックの発生と回避
// NG: UI スレッドや ASP.NET コンテキストで .Result/.Wait() を使う
static async Task<string> GetDataAsync()
{
    await Task.Delay(1000);    // ここで UI スレッドへの復帰を予約
    return "data";
}

// デッドロック発生のメカニズム(WPF / ASP.NET 非 Core):
// 1. UI スレッドが GetDataAsync().Result でブロック
// 2. GetDataAsync は await 完了後に UI スレッドへ戻ろうとする
// 3. しかし UI スレッドは .Result でブロック中 → 永久待機

// NG パターン
// string result = GetDataAsync().Result;  // デッドロック!
// GetDataAsync().Wait();                  // デッドロック!

// 解決策 1: 全部 async にする(推奨)
static async Task CorrectAsync()
{
    string result = await GetDataAsync();  // await で待つ
    Console.WriteLine(result);
}

// 解決策 2: ConfigureAwait(false) を付ける(ライブラリ側の対応)
static async Task<string> GetDataSafeAsync()
{
    await Task.Delay(1000).ConfigureAwait(false);  // UI スレッドへ戻らない
    return "data";
}
// ConfigureAwait(false) を付ければ .Result も(理論上)デッドロックしない
// ただし推奨はしない。全部 async にするのが最善

// 解決策 3: どうしても同期で待ちたい場合
static string GetDataSync()
{
    // Task.Run でスレッドプール上で実行してから同期ブロック
    // (コンテキストが関与しないので安全)
    return Task.Run(() => GetDataAsync()).GetAwaiter().GetResult();
}
黄金律: 非同期処理は async/await で貫く
コードベースの一部だけを非同期にして、途中で .Result/.Wait() で同期化するのが最も危険です。「async は感染する」と言われるように、呼び出し元も async にすることで安全に伝播させましょう。

Task.Run — CPU バウンド処理のオフロード

async/await はI/Oバウンド処理(HTTP、DB、ファイル)に最適化されています。CPU バウンド処理(重い計算、画像処理など)をバックグラウンドで実行するには Task.Run を使います。

Task.Run の適切な使い方
// I/O バウンド: await だけで OK(Task.Run 不要)
static async Task<string> FetchFromDbAsync(int id)
{
    // DbContext.FindAsync などは内部で非同期 I/O を使う
    await Task.Delay(50);  // DB 呼び出しのシミュレーション
    return $"Record-{id}";
}

// CPU バウンド: Task.Run でスレッドプールに移す
static async Task ProcessImageAsync(byte[] data)
{
    // UI スレッドをブロックしないように Task.Run でオフロード
    byte[] result = await Task.Run(() => HeavyImageProcess(data));
    UpdateUI(result);
}

static byte[] HeavyImageProcess(byte[] data)
{
    // 重いCPU処理(Task.Run 内なのでスレッドプールで実行される)
    return data.Select(b => (byte)(255 - b)).ToArray();
}

// NG: ライブラリコードで Task.Run を使う
// ライブラリは呼び出し元が制御すべき。Task.Run を内部で使うと
// スレッドプールのスレッドを2重消費するリスクがある

// NG パターン(ライブラリ内では避ける)
public static Task<int> BadLibraryMethodAsync(int x)
    => Task.Run(() => ExpensiveCalculation(x)); // ライブラリが Task.Run を隠蔽するのは NG

// OK: 同期メソッドとして公開し、呼び出し側が Task.Run を判断
public static int GoodLibraryMethod(int x) => ExpensiveCalculation(x);
// 呼び出し元: var result = await Task.Run(() => GoodLibraryMethod(x));

static int ExpensiveCalculation(int x) => x * x;
static void UpdateUI(byte[] data) { }

Task.WhenAll と Task.WhenAny

Task.WhenAll — 並列実行してすべて完了を待つ
static async Task<int> FetchScoreAsync(string name)
{
    await Task.Delay(new Random().Next(500, 1500));
    return name.Length * 10;
}

static async Task Main()
{
    // NG: 逐次 await — 合計 3 秒以上かかる
    var s1 = await FetchScoreAsync("Alice");  // ~1秒待つ
    var s2 = await FetchScoreAsync("Bob");    // さらに ~1秒待つ
    var s3 = await FetchScoreAsync("Charlie");// さらに ~1秒待つ

    // OK: Task.WhenAll — 並列実行でほぼ最長の 1 件分だけかかる
    var tasks = new[]
    {
        FetchScoreAsync("Alice"),
        FetchScoreAsync("Bob"),
        FetchScoreAsync("Charlie")
    };
    int[] scores = await Task.WhenAll(tasks);
    Console.WriteLine(string.Join(", ", scores));
}
Task.WhenAll の例外収集 — AggregateException
static async Task<int> FetchAsync(int id)
{
    await Task.Delay(100);
    if (id == 2) throw new HttpRequestException($"ID {id} の取得に失敗");
    if (id == 3) throw new TimeoutException($"ID {id} がタイムアウト");
    return id * 100;
}

static async Task Main()
{
    var tasks = new[] { FetchAsync(1), FetchAsync(2), FetchAsync(3) };

    try
    {
        int[] results = await Task.WhenAll(tasks);
    }
    catch (Exception ex)
    {
        // await Task.WhenAll は最初の例外しか再スローしない
        Console.WriteLine($"最初の例外: {ex.Message}");

        // すべての例外を確認するには tasks[i].Exception を見る
        foreach (var t in tasks.Where(t => t.IsFaulted))
        {
            foreach (var inner in t.Exception!.InnerExceptions)
                Console.WriteLine($"  → {inner.GetType().Name}: {inner.Message}");
        }
    }
}
Task.WhenAny — 最初の完了を待つ・タイムアウトパターン
static async Task<string> FetchAsync(string name)
{
    await Task.Delay(new Random().Next(500, 2000));
    return $"{name} の結果";
}

// 最初に完了したタスクの結果を使う
static async Task WhenAnyExample()
{
    var tasks = new[]
    {
        FetchAsync("サーバーA"),
        FetchAsync("サーバーB"),
        FetchAsync("サーバーC"),
    };

    Task<string> winner = (Task<string>)await Task.WhenAny(tasks);
    Console.WriteLine($"最速: {await winner}");
}

// タイムアウトパターン: 3秒以内に完了しなければ諦める
static async Task<string?> WithTimeoutAsync(Task<string> task, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout);
    var completed = await Task.WhenAny(task, timeoutTask);

    if (completed == timeoutTask)
    {
        Console.WriteLine("タイムアウト");
        return null;
    }
    return await task;  // 正常完了
}

// 使用例
var result = await WithTimeoutAsync(FetchAsync("サーバーA"), TimeSpan.FromSeconds(3));
Console.WriteLine(result ?? "取得失敗");

IAsyncEnumerable<T> と await foreach(C# 8+)

大量のデータを一括取得してから処理するのではなく、届いたそばから処理する(非同期ストリーム)が IAsyncEnumerable<T> です。サーバーサイドストリーミング・大量レコードの逐次処理に最適です。

IAsyncEnumerable と await foreach
using System.Runtime.CompilerServices;

// yield return を async メソッドで使う → IAsyncEnumerable<T> を生成
static async IAsyncEnumerable<int> GenerateNumbersAsync(
    int count,
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(200, ct);  // 非同期でデータ生成(API/DB呼び出しを想定)
        yield return i * i;
    }
}

// 消費側: await foreach で逐次処理
static async Task Main()
{
    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

    await foreach (var value in GenerateNumbersAsync(20, cts.Token))
    {
        Console.WriteLine(value);   // 届いたそばから処理
    }
}

// 実践例: DB の大量レコードを逐次ストリーミング
static async IAsyncEnumerable<Order> StreamOrdersAsync()
{
    // EF Core 8 の ToAsyncEnumerable() や
    // 手動 cursor ページングで実装する例
    int page = 0;
    while (true)
    {
        var batch = await FetchOrderPageAsync(page++);
        if (batch.Count == 0) yield break;
        foreach (var order in batch)
            yield return order;
    }
}

static async Task<List<Order>> FetchOrderPageAsync(int page)
{
    await Task.Delay(50);
    return page < 3 ? new List<Order> { new Order(page) } : new List<Order>();
}

record Order(int PageNum);
IAsyncEnumerable vs Task<List<T>>
Task<List<T>>: 全件揃うまで待ってから一括処理。メモリに全件載る。
IAsyncEnumerable<T>: 1件届くたびに逐次処理。メモリ効率が高く、ユーザーへの初回表示も速い。EF Core 8 の ToAsyncEnumerable()・gRPC サーバーストリーミングなどが返す型です。

await using と IAsyncDisposable(C# 8+)

非同期でリソースを解放する必要があるクラス(DB 接続・ネットワークストリームなど)は IAsyncDisposable を実装します。await using で自動的に非同期 Dispose が呼ばれます。

IAsyncDisposable と await using
// IAsyncDisposable を実装するクラス
public class AsyncResource : IAsyncDisposable
{
    private bool _disposed;

    public async Task<string> ReadAsync()
    {
        if (_disposed) throw new ObjectDisposedException(nameof(AsyncResource));
        await Task.Delay(50);
        return "データ";
    }

    // 非同期で解放処理を行う
    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        Console.WriteLine("非同期クリーンアップ開始");
        await Task.Delay(100);  // DB 切断・接続プールへの返却などを想定
        _disposed = true;
        Console.WriteLine("非同期クリーンアップ完了");
    }
}

static async Task Main()
{
    // await using: using ブロック終了時に DisposeAsync() を自動呼び出し
    await using var resource = new AsyncResource();
    var data = await resource.ReadAsync();
    Console.WriteLine(data);
}   // ← ここで await resource.DisposeAsync() が自動実行される

// HttpClient のラッパーなど実際の例
await using var client = new SomeAsyncClient("endpoint");
var response = await client.GetAsync("/api/data");

IProgress<T> — 進捗報告パターン

ファイルダウンロード・バッチ処理などで進捗を UI に通知するには IProgress<T> インターフェースを使います。Progress<T> クラスは UI スレッドで自動的にコールバックを呼び出すため、スレッドセーフです。

IProgress<T> による進捗報告
// 進捗情報を表すクラス
public record DownloadProgress(int Percent, long BytesReceived, long TotalBytes);

// 処理側: IProgress<T> を受け取って Report で通知
static async Task DownloadFileAsync(
    string url,
    IProgress<DownloadProgress>? progress = null,
    CancellationToken ct = default)
{
    long total = 1_000_000;  // 擬似的なファイルサイズ

    for (int i = 0; i <= 10; i++)
    {
        await Task.Delay(200, ct);
        long received = total * i / 10;
        int percent = i * 10;

        // progress が null でないときだけ報告(オプション引数のため)
        progress?.Report(new DownloadProgress(percent, received, total));
    }
}

// UI 側(コンソールアプリ例)
static async Task Main()
{
    // Progress<T>: Report() の呼び出しを作成時のコンテキスト(UI スレッド)で実行
    var progress = new Progress<DownloadProgress>(p =>
    {
        Console.WriteLine($"進捗: {p.Percent}% ({p.BytesReceived:N0} / {p.TotalBytes:N0} bytes)");
    });

    using var cts = new CancellationTokenSource();
    await DownloadFileAsync("https://example.com/file.zip", progress, cts.Token);
    Console.WriteLine("ダウンロード完了");
}

実践パターン

RetryAsync — リトライロジック

RetryAsync 汎用ヘルパー
static async Task<T> RetryAsync<T>(
    Func<Task<T>> operation,
    int maxAttempts = 3,
    TimeSpan? delay = null,
    CancellationToken ct = default)
{
    delay ??= TimeSpan.FromSeconds(1);
    var exceptions = new List<Exception>();

    for (int attempt = 1; attempt <= maxAttempts; attempt++)
    {
        try
        {
            return await operation();
        }
        catch (Exception ex) when (ex is not OperationCanceledException)
        {
            exceptions.Add(ex);
            if (attempt == maxAttempts) break;

            Console.WriteLine($"試行 {attempt}/{maxAttempts} 失敗: {ex.Message}。{delay.Value.TotalSeconds}秒後にリトライ");
            await Task.Delay(delay.Value, ct);
        }
    }

    throw new AggregateException($"最大試行回数 {maxAttempts} 回に達しました", exceptions);
}

// 使用例
string result = await RetryAsync(
    () => FetchFromUnstableApiAsync(),
    maxAttempts: 3,
    delay: TimeSpan.FromSeconds(2)
);

static async Task<string> FetchFromUnstableApiAsync()
{
    await Task.Delay(100);
    if (Random.Shared.Next(3) != 0)
        throw new HttpRequestException("サーバーエラー");
    return "成功";
}

並列度を制限した並列処理

SemaphoreSlim で並列度を制限
// 一度に N 件だけ並列実行(スロットリング)
static async Task<T[]> ParallelForEachAsync<TSource, T>(
    IEnumerable<TSource> source,
    Func<TSource, Task<T>> operation,
    int maxDegreeOfParallelism = 4,
    CancellationToken ct = default)
{
    var semaphore = new SemaphoreSlim(maxDegreeOfParallelism);
    var tasks = source.Select(async item =>
    {
        await semaphore.WaitAsync(ct);
        try   { return await operation(item); }
        finally { semaphore.Release(); }
    });
    return await Task.WhenAll(tasks);
}

// 使用例: 100 件の URL を最大 4 並列でフェッチ
var urls = Enumerable.Range(1, 100).Select(i => $"https://api.example.com/items/{i}");
var results = await ParallelForEachAsync(
    urls,
    async url =>
    {
        await Task.Delay(50);  // HTTP 呼び出しのシミュレーション
        return $"Result-{url}";
    },
    maxDegreeOfParallelism: 4
);
Console.WriteLine($"取得完了: {results.Length} 件");

よくある質問

Qasync/await を使うと常に高速になりますか?
Aいいえ。async/await の恩恵はスレッドの解放(UI の応答性維持・サーバーのスループット向上)であって、個々の処理を速くするものではありません。CPU バウンド処理に単に await を付けても速くはなりません。CPU バウンドは Task.Run でスレッドプールに移すことで UI をブロックせずに済みます。
QConfigureAwait(false) は必ず付けるべきですか?
Aライブラリ・共通処理コードでは付けることを推奨します。UI フレームワーク(WPF/MAUI)のコードビハインドでは付けないでください(await 後に UI スレッドが必要なため)。ASP.NET Core ではどちらでも実質的に同じですが、統一のために付けるプロジェクトもあります。
QTask.WhenAll と await を順番に書く違いは?
A順番に await すると合計時間が各タスクの時間の合計になります。Task.WhenAll で並列実行すると合計時間は最も遅いタスク1つ分になります。独立した複数の非同期処理は Task.WhenAll に渡すのが基本です。
Qasync void はなぜだめなのですか?
A主に3つの理由があります。①例外が呼び出し元に伝播せず、アプリが落ちる。② await できないので完了を待てない(Fire and forget になる)。③ユニットテストが書けない。イベントハンドラー(button.Click += async (s, e) => { })だけが許容される唯一の例外です。
QIAsyncEnumerable と Channel はどう使い分けますか?
AIAsyncEnumerable<T> は「生産者から消費者へ順番に届ける」シンプルなストリームに適しています。Channel<T>System.Threading.Channels)は「複数の生産者と複数の消費者」「バックプレッシャー制御」「有界バッファ」が必要なパイプライン処理向けです。Webスクレイピングやイベントパイプラインは Channel が向いています。

まとめ

トピック ポイント
ステートマシン await 地点が状態になる。既に完了なら即続行(ゼロコスト)
戻り値の型 通常は Task/Task<T>。頻繁に同期完了するなら ValueTask<T>
async void イベントハンドラー専用。それ以外では使用禁止
ConfigureAwait(false) ライブラリコードに付ける。UI コードには付けない
デッドロック .Result/.Wait() を UI/ASP.NET スレッドで使うと発生。全部 async にする
Task.Run CPU バウンド処理をスレッドプールにオフロード。I/O バウンドには不要
Task.WhenAll 独立した複数タスクを並列実行。例外は全タスク完了後に確認
Task.WhenAny 最初の完了を待つ。タイムアウトパターンに有効
IAsyncEnumerable<T> await foreach で逐次消費。大量データ・ストリーミングに最適
IAsyncDisposable await using で非同期 Dispose。DB 接続などに
IProgress<T> UI スレッドセーフな進捗報告。Progress<T> で実装
SemaphoreSlim 並列度を制限した並列処理(スロットリング)

CancellationToken によるキャンセルの詳細は非同期タスクのキャンセル方法を、TaskParallel の使い分けは非同期処理と並列処理の違いを参照してください。