[C#] awaitステートメントの使い方 – 非同期処理を待機する

C#のawaitステートメントは、非同期メソッドの完了を待機するために使用されます。

awaitは、async修飾子が付いたメソッド内でのみ使用可能です。

非同期メソッドは通常、TaskまたはTask<T>を返し、awaitを使うことで、処理が完了するまで他の処理をブロックせずに待機できます。

例えば、await SomeAsyncMethod()と記述することで、SomeAsyncMethodの完了を待ちながら、他のタスクを並行して実行できます。

この記事でわかること
  • awaitステートメントの基本的な使い方
  • 非同期処理の実践例と応用
  • デッドロックやブロックの回避方法
  • 非同期メソッドの例外処理の重要性
  • タイムアウトやキャンセルの設定方法

非同期処理を活用し、アプリケーションのパフォーマンス向上を目指すことが重要。

目次から探す

awaitステートメントの基本的な使い方

非同期メソッドの定義

C#における非同期メソッドは、asyncキーワードを使用して定義されます。

非同期メソッドは、通常のメソッドと同様に引数を受け取りますが、戻り値はTaskまたはTask<T>である必要があります。

これにより、メソッドが非同期に実行されることを示します。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        await SampleAsyncMethod();
    }
    static async Task SampleAsyncMethod()
    {
        // 非同期処理をここに記述
        await Task.Delay(1000); // 1秒待機
        Console.WriteLine("非同期処理が完了しました。");
    }
}
非同期処理が完了しました。

TaskとTask<T>の違い

TaskTask<T>は、非同期メソッドの戻り値として使用される2つの異なる型です。

スクロールできます
説明
Task戻り値がない非同期メソッドに使用
Task<T>戻り値がある非同期メソッドに使用

Task<T>は、非同期処理の結果を返すために使用され、Tは戻り値の型を示します。

awaitを使った非同期メソッドの呼び出し

awaitキーワードを使用することで、非同期メソッドの完了を待機することができます。

awaitは、非同期メソッドの実行を一時停止し、結果が得られるまで待機します。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        int result = await CalculateAsync(5);
        Console.WriteLine($"計算結果: {result}");
    }
    static async Task<int> CalculateAsync(int number)
    {
        await Task.Delay(1000); // 1秒待機
        return number * number; // 数の二乗を返す
    }
}
計算結果: 25

戻り値がない非同期メソッドの扱い

戻り値がない非同期メソッドは、Taskを返す必要があります。

awaitを使用して呼び出す際には、戻り値を受け取る必要はありません。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        await PrintMessageAsync();
    }
    static async Task PrintMessageAsync()
    {
        await Task.Delay(1000); // 1秒待機
        Console.WriteLine("メッセージを表示しました。");
    }
}
メッセージを表示しました。

例外処理とawait

非同期メソッド内で発生した例外は、awaitを使用して呼び出す際にキャッチすることができます。

try-catchブロックを使用して、例外を適切に処理することが重要です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        try
        {
            await ThrowExceptionAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"例外が発生しました: {ex.Message}");
        }
    }
    static async Task ThrowExceptionAsync()
    {
        await Task.Delay(1000); // 1秒待機
        throw new InvalidOperationException("無効な操作です。");
    }
}
例外が発生しました: 無効な操作です。

awaitの動作の仕組み

awaitの内部動作

awaitキーワードは、非同期メソッドの実行を一時停止し、指定されたTaskが完了するのを待機します。

awaitが呼び出されると、現在のメソッドの状態が保存され、制御が呼び出し元に戻ります。

Taskが完了すると、保存された状態が復元され、メソッドの実行が再開されます。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("処理開始");
        await SampleAsyncMethod();
        Console.WriteLine("処理完了");
    }
    static async Task SampleAsyncMethod()
    {
        await Task.Delay(2000); // 2秒待機
        Console.WriteLine("非同期処理が完了しました。");
    }
}
処理開始
非同期処理が完了しました。
処理完了

コンテキストの切り替え

awaitを使用すると、非同期メソッドの実行が一時停止し、呼び出し元のコンテキストに戻ります。

デフォルトでは、awaitは元のコンテキスト(例えば、UIスレッド)で続行されますが、ConfigureAwait(false)を使用することで、コンテキストを無視してバックグラウンドスレッドで実行することも可能です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("処理開始");
        await SampleAsyncMethod().ConfigureAwait(false);
        Console.WriteLine("処理完了");
    }
    static async Task SampleAsyncMethod()
    {
        await Task.Delay(2000); // 2秒待機
        Console.WriteLine("非同期処理が完了しました。");
    }
}
処理開始
非同期処理が完了しました。
処理完了

スレッドのブロックと非ブロック

awaitを使用することで、スレッドをブロックすることなく非同期処理を実行できます。

これにより、UIスレッドがブロックされず、アプリケーションが応答し続けることが可能になります。

非同期メソッドは、I/O操作や長時間の計算を行う際に特に有効です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("処理開始");
        await LongRunningOperationAsync();
        Console.WriteLine("処理完了");
    }
    static async Task LongRunningOperationAsync()
    {
        await Task.Delay(5000); // 5秒待機
        Console.WriteLine("長時間処理が完了しました。");
    }
}
処理開始
長時間処理が完了しました。
処理完了

awaitのパフォーマンスへの影響

awaitを使用することで、アプリケーションのパフォーマンスが向上する場合があります。

特に、I/Oバウンドの操作(ファイルの読み書きやネットワーク通信など)では、awaitを使用することで、スレッドを無駄に消費せずに効率的に処理を行うことができます。

ただし、CPUバウンドの操作では、awaitの使用が必ずしもパフォーマンス向上につながるわけではありません。

using System;
using System.Diagnostics;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        
        await PerformIOOperationAsync();
        
        stopwatch.Stop();
        Console.WriteLine($"処理時間: {stopwatch.ElapsedMilliseconds} ms");
    }
    static async Task PerformIOOperationAsync()
    {
        await Task.Delay(3000); // 3秒待機
        Console.WriteLine("I/O操作が完了しました。");
    }
}
I/O操作が完了しました。
処理時間: 3000 ms

非同期処理の実践例

ファイルの読み書きでのawaitの使用

非同期でファイルの読み書きを行う際には、FileStreamStreamReaderStreamWriterなどのクラスを使用します。

awaitを使うことで、ファイル操作中にアプリケーションが応答し続けることができます。

using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        string filePath = "sample.txt";
        await WriteToFileAsync(filePath, "こんにちは、非同期ファイル操作!");
        string content = await ReadFromFileAsync(filePath);
        Console.WriteLine($"ファイルの内容: {content}");
    }
    static async Task WriteToFileAsync(string path, string content)
    {
        using (StreamWriter writer = new StreamWriter(path))
        {
            await writer.WriteLineAsync(content); // 非同期で書き込み
        }
    }
    static async Task<string> ReadFromFileAsync(string path)
    {
        using (StreamReader reader = new StreamReader(path))
        {
            return await reader.ReadToEndAsync(); // 非同期で読み込み
        }
    }
}
ファイルの内容: こんにちは、非同期ファイル操作!

Web API呼び出しでのawaitの使用

非同期でWeb APIを呼び出す際には、HttpClientクラスを使用します。

awaitを使うことで、APIからの応答を待機しながら、他の処理を行うことができます。

using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        string url = "https://api.github.com/users/octocat";
        string response = await GetApiResponseAsync(url);
        Console.WriteLine($"APIの応答: {response}");
    }
    static async Task<string> GetApiResponseAsync(string url)
    {
        using (HttpClient client = new HttpClient())
        {
            client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3");
            return await client.GetStringAsync(url); // 非同期でAPI呼び出し
        }
    }
}
APIの応答: { ... } // GitHub APIからのJSON応答

データベースアクセスでのawaitの使用

非同期でデータベースにアクセスする際には、Entity FrameworkやDapperなどのORMを使用します。

awaitを使うことで、データベース操作中にアプリケーションが応答し続けることができます。

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        string connectionString = "your_connection_string_here";
        var users = await GetUsersAsync(connectionString);
        foreach (var user in users)
        {
            Console.WriteLine($"ユーザー名: {user}");
        }
    }
    static async Task<List<string>> GetUsersAsync(string connectionString)
    {
        var users = new List<string>();
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            await connection.OpenAsync(); // 非同期で接続
            using (SqlCommand command = new SqlCommand("SELECT Name FROM Users", connection))
            {
                using (SqlDataReader reader = await command.ExecuteReaderAsync()) // 非同期でクエリ実行
                {
                    while (await reader.ReadAsync()) // 非同期で読み込み
                    {
                        users.Add(reader.GetString(0));
                    }
                }
            }
        }
        return users;
    }
}
ユーザー名: ユーザー1
ユーザー名: ユーザー2

UIスレッドでのawaitの使用

UIアプリケーションでは、awaitを使用することで、UIスレッドをブロックせずに非同期処理を実行できます。

これにより、ユーザーインターフェースが応答し続けることができます。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
class Program : Form
{
    private Button button;
    public Program()
    {
        button = new Button { Text = "非同期処理を実行" };
        button.Click += async (sender, e) => await PerformAsyncOperation();
        Controls.Add(button);
    }
    static async Task Main()
    {
        Application.Run(new Program());
    }
    private async Task PerformAsyncOperation()
    {
        button.Enabled = false; // ボタンを無効化
        await Task.Delay(3000); // 3秒待機
        MessageBox.Show("非同期処理が完了しました。");
        button.Enabled = true; // ボタンを再度有効化
    }
}
非同期処理が完了しました。 // メッセージボックスが表示される

awaitの応用

複数の非同期処理を同時に実行する

複数の非同期処理を同時に実行することで、全体の処理時間を短縮できます。

Taskをリストに追加し、awaitを使ってそれらを同時に実行します。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        var tasks = new List<Task>
        {
            PerformTaskAsync("タスク1", 2000),
            PerformTaskAsync("タスク2", 3000),
            PerformTaskAsync("タスク3", 1000)
        };
        await Task.WhenAll(tasks); // 全てのタスクが完了するのを待機
        Console.WriteLine("全てのタスクが完了しました。");
    }
    static async Task PerformTaskAsync(string taskName, int delay)
    {
        await Task.Delay(delay); // 指定された時間待機
        Console.WriteLine($"{taskName} が完了しました。");
    }
}
タスク3 が完了しました。
タスク1 が完了しました。
タスク2 が完了しました。
全てのタスクが完了しました。

Task.WhenAllとTask.WhenAnyの使い方

Task.WhenAllは、すべてのタスクが完了するのを待機します。

一方、Task.WhenAnyは、最初に完了したタスクを待機します。

これにより、特定の条件に基づいて処理を行うことができます。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        var tasks = new List<Task>
        {
            PerformTaskAsync("タスク1", 3000),
            PerformTaskAsync("タスク2", 2000),
            PerformTaskAsync("タスク3", 1000)
        };
        Task firstCompletedTask = await Task.WhenAny(tasks); // 最初に完了したタスクを待機
        Console.WriteLine("最初に完了したタスクが見つかりました。");
    }
    static async Task PerformTaskAsync(string taskName, int delay)
    {
        await Task.Delay(delay); // 指定された時間待機
        Console.WriteLine($"{taskName} が完了しました。");
    }
}
タスク3 が完了しました。
最初に完了したタスクが見つかりました。

非同期処理のキャンセル

非同期処理をキャンセルするには、CancellationTokenを使用します。

これにより、長時間実行される処理を中断することができます。

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var token = cts.Token;
        var task = PerformCancellableTaskAsync(token);
        // 2秒後にキャンセル
        cts.CancelAfter(2000);
        try
        {
            await task; // タスクの完了を待機
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("タスクがキャンセルされました。");
        }
    }
    static async Task PerformCancellableTaskAsync(CancellationToken token)
    {
        for (int i = 0; i < 5; i++)
        {
            token.ThrowIfCancellationRequested(); // キャンセル要求をチェック
            await Task.Delay(1000); // 1秒待機
            Console.WriteLine($"処理中... {i + 1}");
        }
    }
}
処理中... 1
処理中... 2
タスクがキャンセルされました。

非同期処理のタイムアウト設定

非同期処理にタイムアウトを設定することで、指定した時間内に処理が完了しない場合に例外を発生させることができます。

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        cts.CancelAfter(2000); // 2秒後にキャンセル
        try
        {
            await PerformTaskWithTimeoutAsync(cts.Token);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("処理がタイムアウトしました。");
        }
    }
    static async Task PerformTaskWithTimeoutAsync(CancellationToken token)
    {
        await Task.Delay(5000, token); // 5秒待機(キャンセル可能)
        Console.WriteLine("処理が完了しました。");
    }
}
処理がタイムアウトしました。

awaitを使う際の注意点

デッドロックの回避

デッドロックは、非同期処理を使用する際に注意が必要な問題です。

特に、UIアプリケーションでは、awaitを使用する際にUIスレッドをブロックしないようにすることが重要です。

ConfigureAwait(false)を使用することで、元のコンテキストに戻らずに非同期処理を続行することができます。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        await Task.Run(() => PerformTaskAsync());
    }
    static async Task PerformTaskAsync()
    {
        await Task.Delay(1000).ConfigureAwait(false); // UIスレッドをブロックしない
        Console.WriteLine("タスクが完了しました。");
    }
}
タスクが完了しました。

UIスレッドのブロックを防ぐ

UIアプリケーションでは、長時間実行される処理を非同期で実行することで、UIスレッドがブロックされるのを防ぐことができます。

これにより、ユーザーインターフェースが応答し続け、アプリケーションがスムーズに動作します。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
class Program : Form
{
    private Button button;
    public Program()
    {
        button = new Button { Text = "非同期処理を実行" };
        button.Click += async (sender, e) => await PerformLongRunningTaskAsync();
        Controls.Add(button);
    }
    static async Task Main()
    {
        Application.Run(new Program());
    }
    private async Task PerformLongRunningTaskAsync()
    {
        button.Enabled = false; // ボタンを無効化
        await Task.Delay(5000); // 5秒待機
        MessageBox.Show("非同期処理が完了しました。");
        button.Enabled = true; // ボタンを再度有効化
    }
}
非同期処理が完了しました。 // メッセージボックスが表示される

非同期メソッドの例外処理

非同期メソッド内で発生した例外は、awaitを使用して呼び出す際にキャッチすることができます。

try-catchブロックを使用して、例外を適切に処理することが重要です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        try
        {
            await ThrowExceptionAsync();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"例外が発生しました: {ex.Message}");
        }
    }
    static async Task ThrowExceptionAsync()
    {
        await Task.Delay(1000); // 1秒待機
        throw new InvalidOperationException("無効な操作です。");
    }
}
例外が発生しました: 無効な操作です。

非同期処理のキャンセルと例外

非同期処理をキャンセルする際には、CancellationTokenを使用します。

キャンセル要求があった場合には、OperationCanceledExceptionをスローすることができます。

これにより、非同期処理が中断されたことを適切に処理できます。

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main(string[] args)
    {
        var cts = new CancellationTokenSource();
        var token = cts.Token;
        var task = PerformCancellableTaskAsync(token);
        // 2秒後にキャンセル
        cts.CancelAfter(2000);
        try
        {
            await task; // タスクの完了を待機
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("タスクがキャンセルされました。");
        }
    }
    static async Task PerformCancellableTaskAsync(CancellationToken token)
    {
        for (int i = 0; i < 5; i++)
        {
            token.ThrowIfCancellationRequested(); // キャンセル要求をチェック
            await Task.Delay(1000); // 1秒待機
            Console.WriteLine($"処理中... {i + 1}");
        }
    }
}
処理中... 1
処理中... 2
タスクがキャンセルされました。

よくある質問

awaitとasyncの違いは何ですか?

asyncはメソッドの定義に使用されるキーワードで、そのメソッドが非同期であることを示します。

一方、awaitは非同期メソッド内で使用され、指定されたTaskが完了するのを待機します。

asyncを使うことで、メソッドが非同期処理を行うことを示し、awaitを使うことで、その処理の完了を待つことができます。

awaitを使わないとどうなりますか?

awaitを使わない場合、非同期メソッドは即座に制御を返しますが、処理が完了する前に次の行のコードが実行されます。

これにより、非同期処理の結果を待たずに次の処理が進んでしまい、意図しない動作やエラーが発生する可能性があります。

特に、非同期メソッドの結果を必要とする場合には、awaitを使用することが重要です。

static async Task<int> CalculateAsync()
{
    await Task.Delay(1000); // 1秒待機
    return 42;
}
static void Main()
{
    var result = CalculateAsync(); // awaitを使わない
    Console.WriteLine(result.Result); // ここで例外が発生する可能性がある
}

非同期処理のデバッグ方法は?

非同期処理のデバッグは、通常の同期処理と同様に行えますが、いくつかのポイントに注意が必要です。

  1. ブレークポイントの設定: awaitの前後にブレークポイントを設定することで、非同期処理の流れを追うことができます。
  2. タスクの状態の確認: Taskオブジェクトの状態を確認することで、処理が完了しているかどうかを確認できます。
  3. 例外のキャッチ: 非同期メソッド内で発生した例外は、awaitを使用して呼び出す際にキャッチできます。

try-catchブロックを使用して、例外を適切に処理します。

  1. デバッグ出力: Console.WriteLineやデバッグ出力を使用して、処理の進行状況を確認することができます。

これらの方法を組み合わせることで、非同期処理のデバッグを効果的に行うことができます。

まとめ

この記事では、C#におけるawaitステートメントの使い方やその動作の仕組み、非同期処理の実践例、応用方法、注意点について詳しく解説しました。

非同期処理を適切に活用することで、アプリケーションのパフォーマンスを向上させ、ユーザー体験を向上させることが可能です。

これを機に、非同期プログラミングの技術を実際のプロジェクトに取り入れて、より効率的なコードを書くことに挑戦してみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す