C#
async

async/awaitについての備忘録

最近、業務で非同期処理を使う機会があり
async/awaitについて調べたことを
忘れないうちにまとめたいと思います。

async/await句の使い方

async/await
//async句をつけたメソッドはAsyncのsuffixを付けることが推奨されています。
private async Task<string> HeavyProcessingAsync()
{
    string hoge = "hoge";

    //重たい処理

    return "hoge";
}

private async void Page_Load(object sender, EventArgs e)
{
    //処理A

    string hoge = await HeavyProcessingAsync(); 

    //処理B
}

async句をつけたメソッドの戻り値はTask, Task<T>にします。
Task<T>といっても受け取る際にはTaskを意識せずT型で受け取ることが出来ます。

通常、async句をつけていない重たいメソッドを呼び出した場合、
待機中になってしまい画面が固まってしまいます。
これは使用者からすると不快感に繋がったり、
不信感を与えかねません。

Asyncメソッド内の処理は非同期で行われ、
UIスレッドの自由が利くために別スレッドで処理が行われている間も
画面操作が可能になります。

また、await句をつけることで非同期の処理を
UIスレッドに同期させることが出来ます。

注意が必要なのはawait句はasyncメソッド内でしか使えず、
非同期メソッド呼び出すメソッドがまた非同期になることです。

最終的にイベントメソッドなどへ到達することで
非同期の連鎖を断ち切ることが出来ますが
不要な非同期メソッドを作らないような設計が必要になります。

async/await, Taskのタブー①

非同期メソッド内ではUI操作をすることができません。
以下で非同期メソッド内からのUI操作方法をまとめています。
別スレッドからのUI操作

async/await, Taskのタブー②

以下もやってはいけないコードになります。

タブー
private void Main()
{
    Task.Wait();   
    //あるいはTask.Result();
}

awaitと、Task.Wait()の違いは
英語の自動詞と他動詞の語源を
考えるとわかりやすいです。

await → 他動詞、~を待つ
wait   → 自動詞、(自分が)待つ

awaitでは呼び出し側のスレッドを待機させずに
非同期処理を進めていき完了を待ちます。
これに対し、Task.Wait()をした場合は
呼び出し側スレッドを待機させて非同期処理の完了を待ちます。

これの何がいけないのかというと
WaitしているTaskメソッド内でawaitが使われていた場合です。
この場合にはawaitでスレッドを同期させようにも
呼び出し側スレッドが待機中となっており、
結果デッドロックが発生します。

Wait + await
 UIスレッド→→Wait DeadLock!!
 別スレッド→→→→→→↑ await

await使ってないからいいじゃん!
という場合もその後の改修などで
awaitする必要が出てきた場合にデッドロックを起こしてしまうので、
Task.Wait()は使わない方が懸命と思われます。

また以上のことはTask.Result()でもスレッドを待機させるので
同様にいえます。

デッドロック回避法

また、デッドロックを回避する方法に
ConfigureAwaitをfalseにする方法があります。
コードは以下になります。

デッドロック回避
private async void Page_Load(object sender, EventArgs e)
{
    await HeavyProcessingAsync().ConfigureAwait(false);
}

デフォルトではConfigureAwaitはtrueになっているのですが
trueの場合awaitで非同期処理を同期させた後の処理をUIスレッドにコンテキストスイッチします。

ConfigureAwait(true) (デフォルト値)
 UIスレッド→→→→→→
 別スレッド→→↑ await

ConfigureAwaitをfalseした場合にはawait以降の処理について
再度非同期の処理で再開します

ConfigureAwait(false)
 UIスレッド→→→→→→→→→
 別スレッド→→↑ await→→→

タスクのキャンセル方法

非同期ではじめた処理をトランザクションのロールバックのように、
途中でキャンセルして取り消すことができます。

タスクのキャンセル
private CancellationTokenSource tokenSource = null;

private async void Page_Load(object sender, EventArgs e)
{
    try
    {
        using(this.tokenSource = new CancellationTokenSource())
        {
          string hoge = await HeavyProcessingAsync(tokenSource.Token);
        }
    }
    catch (OperationCancellationException)
    {
        throw; 
    }
}

private void btnCancel_Click(object sender, EventArgs e)
{
    if (tokenSource == null) { return; }

    tokenSource.Cancel();
}

private async Task<string> HeavyProcessingAsync(CancellationToken token)
{
    string hoge = "hoge";

    //重たい処理

    token.ThrowIfCancellationRequested();

    // ↑以下のコードのシンタックスシュガーになります
    // if (token.IsCancellationRequested) 
    // {
    //     throw new OperationCanceledException(token)
    // }

    //重たい処理

    return hoge;
}

Task型のメソッドの引数にCancellationTokenを指定しておきます。

仕組みとして、
btnCancelをクリックしたときにTokenのリソースが生きていれば
tokenSourceからCancel要求が引数を通じてTaskへと送られます。

Task内でCancel要求を受け取った場合、
そのまんまの直訳になりますが
token.ThrowIfCancellationRequested();
の一文でCancel要求がもしあれば
OperationCancellationExceptionをtokenがthrowします。

そのExceptionを呼び出し元メソッドでcatchすることで
タスクのキャンセルが可能になります。


参考
できる!C#で非同期処理(Taskとasync-await)
非同期タスクまたはタスクの一覧のキャンセル (C#) | Microsoft Docs