C#のasync/awaitを完全解説!10のサンプルコード付き

C#のasync/awaitを学ぶ初心者のための解説記事のサムネイル C#
この記事は約15分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミングでは、効率的かつスムーズなコードの実行が重要です。

特に、C#言語における「async」と「await」のキーワードは、この目的を達成するために不可欠です。

本記事では、C#における非同期プログラミングの鍵となるこれらのキーワードについて、初心者でも理解しやすいように徹底的に解説します。

さらに、実際のサンプルコードを用いて、その使い方を紹介していきます。

この記事を読めば、あなたも非同期プログラミングの基本から応用までをマスターできるでしょう。

●C#のasync/awaitとは

C#における「async」と「await」は、非同期プログラミングを行うためのキーワードです。

これらは、プログラムが複数のタスクを同時に処理するのを助け、特に長時間実行する処理やネットワークリクエストの際に役立ちます。

非同期プログラミングを使えば、一つのタスクが完了するのを待つ間に、他のタスクを実行することができ、全体の処理速度を向上させることができます。

「async」キーワードは、メソッドが非同期であることを表します。

このキーワードをメソッドに付けると、そのメソッドは非同期タスクを実行するための特殊なメソッドとなります。

一方、「await」キーワードは、非同期メソッドの実行が完了するまで待機することを表します。

これにより、非同期メソッドの結果を同期的に扱うことができるのです。

非同期プログラミングを使用すると、UIが応答しなくなる「フリーズ」を防ぐことができます。

たとえば、ユーザーインターフェース(UI)を持つアプリケーションでデータをロードする際、非同期プログラミングを使用すると、データのロード中でもUIは動作し続けることができます。

これにより、ユーザー体験が大幅に向上します。

○async/awaitの基本

非同期プログラミングの基本的な使い方を理解するためには、まず「Task」というC#の型を理解する必要があります。

Taskは非同期操作の結果を表す型で、asyncメソッドから戻り値として返されます。

Taskを使用することで、非同期操作の完了を待つことができるようになります。

例えば、あるデータをインターネットからダウンロードする操作があるとします。

この操作を非同期メソッドで行う場合、メソッドはTask型を返し、そのタスクが完了するまでの間、プログラムは他の操作を行うことができます。

そして、データのダウンロードが完了したら、awaitキーワードを使用してその結果を取得します。

async/awaitを使用することで、非同期プログラミングを行う際のコードが読みやすく、理解しやすくなります。

これは、非同期操作を行うコードを書く際に一般的なコールバック関数やプロミスを使用する方法に比べ、コードの見通しが良いためです。

コールバック関数を多用すると、いわゆる「コールバック地獄」に陥りがちですが、async/awaitを使うことで直線的なコードの流れを保つことが可能になります。

●async/awaitの使い方

C#におけるasync/awaitの使い方を理解することは、非同期プログラミングをマスターする上で不可欠です。

ここでは、async/awaitを使った非同期処理の基本的な例を紹介していきます。

○サンプルコード1:非同期メソッドの基本形

非同期メソッドの基本的な構造を見てみましょう。

async Task<int> FetchDataAsync()
{
    await Task.Delay(1000); // 擬似的なデータ取得処理の遅延を模倣
    return 42; // データ取得が完了したと仮定して、値を返す
}

この例では、FetchDataAsyncメソッドは非同期で実行され、Task.Delayによって1秒間の遅延が発生します。この遅延はデータを取得するための時間を模倣しています。

遅延後、メソッドは42という整数を返します。この値は非同期操作の結果として考えることができます。

○サンプルコード2:複数の非同期タスクの実行

複数の非同期タスクを同時に実行することも可能です。

例えば、複数のデータソースから同時にデータを取得する場合、各データソースからのデータ取得を個別の非同期タスクとして実行し、すべてのタスクが完了するのを待つことができます。

async Task FetchMultipleDataAsync()
{
    var task1 = FetchDataAsync(); // データソース1からのデータ取得
    var task2 = FetchDataAsync(); // データソース2からのデータ取得

    await Task.WhenAll(task1, task2); // 両方のタスクの完了を待機
    // ここで、task1とtask2の結果が利用可能
}

この例では、FetchDataAsyncメソッドが2回呼び出され、2つの非同期タスクが生成され

ます。Task.WhenAllを使用して、これらのタスクが完了するのを待ちます。

この方法により、複数の非同期操作を効率的に管理することができます。

○サンプルコード3:例外処理の実装

非同期メソッドでは、例外処理も重要です。

async/awaitを使用する場合、例外はawaitされたタスクの中で発生し、呼び出し元のメソッドまで伝播します。

このため、非同期メソッドを呼び出す際にはtry/catchブロックを使用して例外を捕捉することが推奨されます。

async Task FetchDataWithExceptionHandlingAsync()
{
    try
    {
        await FetchDataAsync(); // データ取得を試みる
    }
    catch (Exception ex)
    {
        // 例外処理
        Console.WriteLine($"エラーが発生しました: {ex.Message}");
    }
}

この例では、FetchDataAsyncメソッドから発生する可能性のある例外を捕捉し、エラーメッセージを表示しています。

○サンプルコード4:async/awaitを使ったデータ取得

最後に、async/awaitを使用して実際にデータを取得する一般的な例を見てみましょう。

この例では、外部のAPIからデータを非同期的に取得します。

async Task<string> FetchDataFromApiAsync()
{
    using (var httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync("https://example.com/api/data");
        response.EnsureSuccessStatusCode();
        var data = await response.Content.ReadAsStringAsync();
        return data; // 取得したデータを返す
    }
}

このコードでは、HttpClientを使用して外部APIにリクエストを送り、非同期的にレスポンスを取得しています。

取得したデータは文字列として返されます。

●async/awaitの応用例

C#のasync/awaitは、基本的な使い方だけでなく、多岐にわたる応用が可能です。

非同期プログラミングの応用は、より複雑なシナリオや、特定の要件を満たすためのカスタマイズされたソリューションを提供します。

ここでは、非同期処理のキャンセル、進行状況の報告、非同期ストリームの使用といった応用例を見ていきましょう。

○サンプルコード5:非同期処理のキャンセル

非同期操作をキャンセルする能力は、長時間実行されるタスクや、ユーザーが操作を中断できるべき場面で重要です。

C#では、CancellationTokenを使って非同期タスクをキャンセルすることができます。

async Task FetchDataWithCancellationAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
        return; // キャンセルされた場合、早期に処理を終了

    await Task.Delay(5000, cancellationToken); // タスクのキャンセルをサポート
    // データ取得処理をここに記述
}

このコードでは、CancellationTokenがタスクの開始時とTask.Delayの呼び出し時にチェックされ、キャンセル要求があればタスクを早期に終了します。

○サンプルコード6:非同期処理の進行状況の報告

長時間実行される非同期処理では、進行状況をユーザーに報告することが望ましい場合があります。

これはIProgress<T>インターフェイスとProgress<T>クラスを使用して実現できます。

async Task FetchDataWithProgressAsync(IProgress<int> progress)
{
    for (int i = 0; i <= 100; i += 20)
    {
        await Task.Delay(1000); // 模擬処理
        progress.Report(i); // 進行状況の報告
    }
}

この例では、データ取得処理を模擬するためにループとTask.Delayを使用しています。

各ステップでprogress.Reportを呼び出して進行状況を報告しています。

○サンプルコード7:非同期ストリームの使用

C# 8.0以降では、IAsyncEnumerable<T>を使用して非同期ストリームを実装できます。

これにより、データのチャンクが利用可能になるとすぐにそれを消費することができます。

async IAsyncEnumerable<int> FetchDataAsyncStream()
{
    for (int i = 0; i < 5; i++)
    {
        await Task.Delay(1000); // データ取得の模擬
        yield return i; // データのチャンクを返す
    }
}

このコードでは、yield returnを使ってデータの各チャンクを非同期的に返しています。

これにより、データが準備され次第、それを処理することができます。

○サンプルコード8:非同期ラムダ式の利用

C#では、ラムダ式を使用して簡潔に非同期操作を記述することができます。

ラムダ式を使うと、コードの可読性を高め、より簡潔に非同期処理を実装できます。

下記の例では、非同期ラムダ式を使用して簡単なデータ取得処理を実行します。

Func<Task<int>> fetchDataAsync = async () =>
{
    await Task.Delay(1000); // 模擬的な遅延
    return 42; // データ取得が完了したと仮定して、値を返す
};

この例では、fetchDataAsyncは非同期ラムダ式を用いて定義されています。

この式はTask<int>を返し、内部で非同期操作を行います。

○サンプルコード9:非同期LINQクエリ

LINQ(Language Integrated Query)は、データをクエリするための強力な機能です。

C#では、非同期LINQクエリを使用して、非同期的にデータを取得することも可能です。

下記のサンプルでは、非同期的にデータを取得するためのLINQクエリを実装します。

async Task<IEnumerable<int>> FetchDataAsyncQuery()
{
    var data = new List<int> { 1, 2, 3, 4, 5 };
    return await Task.Run(() => 
        from d in data
        where d > 2
        select d
    );
}

このコードでは、Task.Runを使用してバックグラウンドでLINQクエリを実行しています。

これにより、メインスレッドをブロックせずにデータ処理を行うことができます。

○サンプルコード10:非同期イベントハンドラ

イベントハンドラは、アプリケーションにおいてユーザーのアクションやシステムイベントに反応する重要な部分です。

C#では、非同期イベントハンドラを実装することで、イベント処理中に非同期操作を行うことが可能です。

async void OnButtonClicked(object sender, EventArgs e)
{
    await Task.Delay(1000); // 模擬的な処理遅延
    Console.WriteLine("ボタンがクリックされました");
}

このサンプルでは、ボタンのクリックイベントを処理する非同期イベントハンドラOnButtonClickedを表しています。

awaitを使用することで、UIスレッドをブロックせずに遅延処理を行います。

●注意点と対処法

非同期プログラミングは多くのメリットを提供しますが、注意すべき点もいくつかあります。

正しく理解し適切に使用しないと、思わぬ問題やパフォーマンスの低下を引き起こす可能性があります。

ここでは、非同期プログラミングの際に遭遇しがちな問題と、その対処法について説明します。

○非同期プログラミングの落とし穴

デッドロックはメインスレッドで非同期メソッドの結果を同期的に待つと発生する可能性があります。

これは、非同期メソッドがメインスレッドに戻る必要があるが、メインスレッドが結果を待っているために戻れない状態です。

この問題を避けるためには、非同期メソッドの結果を待つ際には常にawaitキーワードを使用します。

また、非同期メソッド内で発生した例外をキャッチして適切に処理しないと、プログラムの予期せぬ終了を引き起こすことがあります。

非同期メソッドでは、例外を適切にキャッチし、必要に応じて処理することが重要です。

さらに、非同期タスクを大量に同時に実行しすぎると、システムのリソースを過剰に消費してしまうことがあります。

タスクの量を制御し、必要に応じて適切な数に調整することが重要です。

○パフォーマンスへの影響と最適化

非同期プログラミングはパフォーマンスの向上に寄与しますが、誤った使い方をすると逆効果になることがあります。

非同期処理の適切な使用として、すべてのメソッドを非同期にする必要はありません。

特に、短時間で完了する処理や、非同期化によるメリットが少ない処理では、同期的な実行の方が適切な場合があります。

また、非同期タスクを適切に管理し、リソースの消費を抑えることが重要です。

Task.WhenAllTask.WhenAnyを使って、タスクの完了を効率的に管理します。

非同期メソッドの適切な設計では、戻り値の型や、例外処理、キャンセル処理などを適切に考慮することが重要です。

これにより、メソッドの再利用性とメンテナンス性が向上します。

●カスタマイズ方法

C#の非同期プログラミングでは、特定のニーズに合わせてカスタマイズすることが可能です。

非同期処理のカスタマイズは、アプリケーションのパフォーマンスを向上させるだけでなく、より良いユーザーエクスペリエンスを提供するためにも重要です。

ここでは、非同期処理をカスタマイズする方法と、非同期パターンの応用について詳しく説明します。

○非同期処理のカスタマイズ

非同期処理をカスタマイズするためには、処理の性質やアプリケーションの要件に応じて適切な戦略を選択する必要があります。

例えば、非同期処理の実行順序を制御するために、Task.WhenAllTask.WhenAnyを使用することができます。

また、非同期処理のキャンセルやタイムアウトの管理、例外処理のカスタマイズなど、異なるシナリオに応じたカスタマイズが可能です。

async Task FetchDataWithCustomCancellationAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
        return; // キャンセルされた場合の処理

    try
    {
        await Task.Delay(10000, cancellationToken); // キャンセル可能な遅延
        // データ取得処理
    }
    catch (TaskCanceledException)
    {
        // キャンセル時の例外処理
    }
}

このコードでは、キャンセルトークンを使用して非同期タスクのキャンセルを管理しています。

Task.Delayにキャンセルトークンを渡すことで、タスクがキャンセルされた場合にTaskCanceledExceptionがスローされます。

○非同期パターンの応用

非同期プログラミングの応用では、特定のパターンやフレームワークを用いて、複雑な非同期処理を簡潔に記述することができます。

例えば、asyncawaitを使用した非同期ストリームは、非同期にデータを生成し消費する際に有効です。

また、TPL(Task Parallel Library)を使用して並列処理を行う方法や、非同期コールバックを使用する方法など、様々な非同期パターンが存在します。

async IAsyncEnumerable<int> FetchDataAsyncStream(CancellationToken cancellationToken)
{
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(1000, cancellationToken); // 非同期処理の遅延
        yield return i; // データのチャンクを返す
    }
}

このコードでは、asyncyield returnを使用して非同期にデータを生成しています。

消費側では、await foreachを使用してこれらのデータを順に処理することができます。

まとめ

本記事では、C#におけるasync/awaitの基本から応用までを網羅的に解説しました。

この記事を通じて、読者の皆さんがC#のasync/awaitを使った非同期プログラミングの基本を理解し、実践に活かすことができるようになれば幸いです。

非同期プログラミングは、多くの場面でアプリケーションの応答性と効率を向上させるため、これらの概念とテクニックを身につけることは非常に価値があります。