LoginSignup
8
2

More than 1 year has passed since last update.

awaitできるもの

await できるのは、GetAwaiter という名前のメソッドを持つ任意のオブジェクトです。
一段メソッドが挟まっており、さらに名前さえ一致すればどんなメソッドでもよいため、拡張性と柔軟性を持っています。
例えば、最もよく使われるTask の他にも、値型のValueTask などがawaitすることを想定した型として標準で用意されています。

GetAwaiter メソッドが返す値も、先程のAwaiterの条件を満たせば何でもよいです。具体的には

public class Awaiter<T> : System.Runtime.CompilerServices.INotifyCompletion
{
    // 条件1
    public bool IsCompleted { get; }

    // 条件2
    public void OnCompleted(Action action)
    {
    }

    // 条件3
    public T GetResult()
    {
    }
  • System.Runtime.CompilerServices.INotifyCompletion インターフェイスの実装
  • 実行が終了したかどうかを示す IsCompleted という名前のプロパティの実装
  • 実行が完了していないときに、完了後に実行する関数を登録できる OnCompleted メソッドの実装
  • (実行が完了していない時は実行が完了するまで同期待ちし)結果を返す GetResult メソッドの実装

の4点です。

メソッド呼び出しの抽象化

ここで一旦、メソッドを呼び出し、その結果を受け取るというプロセスについて考えてみましょう。
メソッドを呼び出すと、その呼び出しが完了したタイミングで制御フローが返り、存在する場合戻り値が得られます。
もう少し低レベルな、IL(中間言語)的な話をするならば、call 命令を実行すると、0個以上の戻り値が評価スタックにプッシュされ、その後 call 命令の次の命令が実行されます。(callvirt命令やcalli命令でもこの点は同じです。)

ところで、制御フローすなわちリソースの利用権とは貴重なものです。特にGUIを持つアプリケーションでは、「UIスレッド」という固有の制御フローを皆で利用しないといけないのですから大変貴重です。しかし、メソッドの呼び出しが制御フローに依存している場合、例えば計算機資源が必要なのではなくネットワーク上での通信待ちが必要なのだとしても制御フローを保持せねばならず、この貴重な資源の効率的な利用を妨げてしまいます。

あるいは、メソッドの実行結果を評価スタックにプッシュするというのも厳しい制約です。評価スタックというIL上の概念は、実際にはCPUのレジスタである可能性もあるため、評価スタック上の値にはアドレスなどの概念はなく、呼び出された関数本人がその場で値を返すしか選択肢がないのです。これでは、メソッドの実行を別のスレッドや別のプロセスに委譲することができず、せっかくマルチコアプロセッサの時代であるのにUIスレッドを占有し続けることになっていまいます。

そこで、従来のメソッド呼び出しという概念を抽象化し、制御フローや評価スタックに依存しないような「新しいメソッド呼び出し」を考えてみましょう。
共有オブジェクトが存在して

  1. 呼び出しが完了したかどうかが判別できる
  2. 呼び出しが完了したあとに実行される命令を指定できる
  3. 呼び出しの結果(戻り値)を受け取ることができる

の3つの条件が満たせるならば、従来 return や制御フローが担っていた機能を任せられます。
Task などのawait可能な型は、同期/非同期を問わず、この3要件でメソッドを抽象化するともいえるのです。
そしてこの抽象化されたメソッドを呼び出すことを明示するのが await です。
この抽象化を採用したからといって非同期になるわけではありません。しかし、これによって結果返却を伴わずにreturnすることが可能になり、結果が出る前にreturnせねばならない非同期が可能になるのです。

例えば、以下のような2つのメソッドがあったときに Hoge()await HogeAsync() それぞれの違いは何でしょうか。
await をつけると非同期で実行される、、、とは限りません。

abstract class Sample
{
    // 実装はわからない
    public abstract int Hoge();
    public abstract Task<int> HogeAsync();
}

class Sample2 : Sample
{
    public override int Hoge()
    {
        System.Threading.Thread.Sleep(1000);
        return 1000;
    }

    public override async Task<int> HogeAsync()
    {
        System.Threading.Thread.Sleep(1000);
        return 1000;
    }
}

上記の Sample2 クラスの HogeAsyncは同期的に実行されます。それでも、Hogeは戻り値として直接に制御と値を返し、HogeAsyncはTaskというオブジェクトを介して制御と値を返します。

awaitの正体

このような抽象化されたメソッドを呼び出すのがawaitですが、そのしくみはどうなっているでしょうか。
コンパイル結果的には、await はステートマシンを生成します。シンプルな await コードのコンパイル結果を覗いてみましょう。

await Task.Delay(100);
Console.WriteLine("waited");

このコードをコンパイルした結果を逆コンパイルしてみると

// Program.<<Main>$>d__0
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

[CompilerGenerated]
private sealed class <<Main>$>d__0 : IAsyncStateMachine
{
    public int <>1__state;

    public AsyncTaskMethodBuilder <>t__builder;

    public string[] args;

    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                awaiter = Task.Delay(100).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <<Main>$>d__0 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                num = (<>1__state = -1);
            }
            awaiter.GetResult();
            Console.WriteLine("waited");
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult();
    }

    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

自動生成コードのためやや難解ですが、中核となる MoveNext メソッドの動作を簡単に説明すると、

  1. Task.Delay を呼び出す。その戻り値を GetAwaiter して Awaiter を取得する。
  2. Awaiter が終了状態か IsCompleted プロパティで判断する。終了状態なら、4へ飛ぶ。そうでなければ、完了時に自分自身を呼び出すよう OnCompleted メソッドで登録し、ステートマシンを待機状態に設定して return する。
  3. Task.Delay が完了し、MoveNext がコールバックされる。ステートマシンが待機状態なので、終了報告なのだとわかる。
  4. 結果を受け取り、ステートマシンの状態をリセットする。
  5. Console.WriteLine を呼び出す。

同じ MoveNext を呼び出しても、内部の状態により実行されるコードが切り替わります。
また元々のコードの await 以後のコードはステートマシン内部に取り込まれてしまい、元の関数には残りません。

2 => 3 ではコールバック関数が発火する必要があるため、制御フローがイベントループまで戻っていることが必須です。
このことが、awaitasync でしか使えず...というasync/await地獄を引き起こす原因の1つです。

そうは言っても、このように、「コンパイラがものすごく頑張る」ことによって、非同期を従来に近い書き味で書くことが可能になっているのです。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2