非同期プログラミング

asyncとawaitは、C# 5.0 (.NET Framework 4.5) 以降で利用できます。それより前の環境で用いると「Could not load type 'System.Runtime.CompilerServices.IAsyncStateMachine'」としてTypeLoadException例外が発生するか、「error CS1061: 'Task' に 'GetAwaiter' の定義が含まれておらず、型 'Task' の最初の引数を受け付ける拡張メソッド 'GetAwaiter' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。」としてコンパイル エラーとなります。

async

メソッドが非同期であることを示します。

public async Task Method()
{
}

戻り値の型 (return type)

用途
Task<TResult> 値を返す、非同期メソッド
Task 値を返さない、非同期メソッド
void 非同期イベントハンドラ
非同期の戻り値の型 (C#) | Microsoft Learn

async void

イベントハンドラ以外では、asyncメソッドでvoidを返さないようにします。voidが返されると、呼び出し側は処理の完了を待たずに処理を続行します。また非同期メソッドで発生した例外は、処理を待機した場合は再スローされますが、そうではない場合は捕捉しなければ失われます。Compiler Warning (level 1) CS4014 | Microsoft Learn

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Method();
    }
    catch (Exception)
    {
        // 例外は捕捉されない
    }
}

private async void Method()
{
    throw new InvalidOperationException();
}

一方でTask.Wait()で同期する場合には、すべての例外はAggregateExceptionで投げられます。非同期メソッドの構文(3/3) - @IT 鈴木孝明 (2012/09/25)

適用可能なメソッド

asyncを適用できるのは、次のメソッドに限られます。

非同期ラムダ (async lambdas)

ラムダ式に非同期処理を適用するには、次のように記述します。

button.Click += async (sender, e) => {};
非同期ラムダ - ラムダ式 (C# プログラミング ガイド) | Microsoft Learn

匿名メソッドでは次のようにします。

button.Click += async delegate( object sender, EventArgs e ) {};

もしawaitを含むラムダ式で「error CS4034: 'await' 演算子は、非同期の ラムダ式 でのみ使用できます。この ラムダ式 を 'async' 修飾子でマークすることを検討してください。」としてエラーとなるときには、次のようにasyncを指定します。

Task task = Task.Run(async () =>
{
    await Method();
});

コンストラクタ

コンストラクタにはasyncを適用できないため、Factory Methodで代用します。c# - Can constructors be async? - Stack Overflow

public static async Task<MyClass> FactoryMethod()
{
    MyClass result = await Task.Run(() =>
    {
        // 時間のかかる処理
        return new MyClass();
    });

    return result;
}

引数にrefまたはoutが指定されたメソッド

error CS1988: 非同期メソッドには ref または out パラメーターを指定できません。」として、パラメータ修飾子を指定したメソッドには、asyncを指定できません。

c# - How to write an async method with out parameter? - Stack Overflow

待たせないタスクへの警告の抑制

asyncを指定したメソッドにawaitで待たせないタスクがあると、CS4014で警告されます。

public void Method1()
{
    Task.Run(() => { }); // ok。asyncを指定していない
}

public async Task Method2()
{
    Task.Run(() => { }); // warning CS4014: この呼び出しを待たないため、現在のメソッドの実行は、呼び出しが完了するまで続行します。呼び出しの結果に 'await' 演算子を適用することを検討してください。

    await Task.Run(() => { });        // ok。awaitで待たせている

#pragma warning disable 4014
    Task.Run(() => { });              // ok。プリプロセッサ ディレクティブで、警告を抑制
#pragma warning restore 4014

    Task dummy = Task.Run(() => { }); // ok。変数に代入すれば、警告されない
}

c# - Suppressing "warning CS4014: Because this call is not awaited, execution of the current method continues..." - Stack Overflow

await

非同期で実行されるTaskの完了を待たせられます。

public async Task<int> Method2()
{
    int result = await Task.Run(() => // ここで呼び出し元へ制御が戻される
    {
        // これ以降は別スレッドとなるため、ここで時間のかかる処理をする
        return 123;
    });

    // これ以降はタスクの完了後に処理される。呼び出し元と同一のスレッドとは限らない
    return result;
}

public async Task Method1()
{
    Task<int> task = Method2();

    // 呼び出し元へ制御を戻し、taskの完了まで処理を中断する
    int a = await task;

    // taskの完了後に、ここから再開される
}

非同期コンテキスト (async context) によってはawait以降の処理が呼び出し元と同一のスレッドに戻されるため、それが不要ならばConfigureAwait(false)を指定します。

Task<int> task = Method2();
int a = await task;

の記述は、

int a = await Method2();

としても同じです。このとき戻り値がないか、あっても不要ならば、

await Method2();

とも記述できます。

非同期メソッドの単体テスト

[TestMethod]
public void TestMethod
{
    AsynchronousMethod(); // 非同期で処理されるメソッド
}

こうすると非同期のメソッドから結果が返される前にテストメソッドが終了するため、正しくテストされません。よって、

[TestMethod]
public async void TestMethod
{
    await AsynchronousMethod();
}

のようにawait演算子を指定すればメソッドの終了まで待機されますが、Visual Studioのテスト (MSTest) はvoidを返す非同期メソッドを無視するため、テストが実行されなくなります。

よってawaitで待機させつつ、テストメソッドからはTaskを返すようにします。

[TestMethod]
public async Task TestMethod
{
    await AsynchronousMethod();
}

await以降に継続する処理がある場合

単体テストから実行した場合、await以降は呼び出し元とは異なるスレッドとなります。c# - Async does not continue on same thread in unit test - Stack Overflow

int thread1, thread2, thread3;

thread1 = Thread.CurrentThread.ManagedThreadId; // 9
await Task.Run(() =>
{
    thread2 = Thread.CurrentThread.ManagedThreadId; // 10
});

thread3 = Thread.CurrentThread.ManagedThreadId; // 10 (ここでは非同期メソッドと同一のスレッドとなっている)
Microsoft Learnから検索