読者です 読者をやめる 読者になる 読者になる

うならぼ

どうも。

async/awaitで一時停止可能なメソッドを作ってみる

サンプルは「呼び出すたびに開始と再開を繰り返すコルーチン的なもの」です。ボタンのイベントハンドラに割り当てると楽しいかもしれません。

前にも EnumerableEx.Create を作るためにawaitableなクラスを作りましたが、その時と比べると随分シンプルです。

public interface IAwaitable : ICriticalNotifyCompletion
{
    IAwaitable GetAwaiter();
    bool IsCompleted { get; }
    void GetResult();
}

public class YieldState : IAwaitable
{
    private Action _continuation;

   #region IAwaitable
    IAwaitable IAwaitable.GetAwaiter() => this;
    bool IAwaitable.IsCompleted => false;
    void IAwaitable.GetResult() { }
    [System.Security.SecurityCritical]
    void ICriticalNotifyCompletion.UnsafeOnCompleted(Action continuation) { _continuation = continuation; }
    void INotifyCompletion.OnCompleted(Action continuation) { _continuation = continuation; }
   #endregion

    public IAwaitable Yield() => this;
    public bool TryResume()
    {
        if (_continuation != null)
        {
            var cont = _continuation;
            _continuation = null;
            cont();
            return true;
        }
        else return false;
    }
}

で、こんな感じに使います。

void Main()
{
    for (int i = 0; i < 6; ++i)
        Foo();
}
YieldState fooState = new YieldState();
async void Foo()
{
    if (fooState.TryResume()) return;

    for (int i = 0; i < 2; ++i)
    {
        Console.WriteLine(i);
        await fooState.Yield();
    }
    Console.WriteLine("done.");
}

この実装は同期コンテキストの調整をしていませんから、TryResume() したコンテキストで続きが実行されることになります。まあイベントハンドラみたいな場面では特に問題ないですね。

例外処理もイベントハンドラがTaskを返せないので無視していますが、async Task に変えたうえでawaitなりContinueWithすれば処理することができます。再開した場所で捕捉したい場合は・・・前回の MoveNext() のように、Taskに格納された例外を投げなおすような実装が必要になります。

ちなみに、別の場所で立てるフラグを非同期に待ちたいだけなら、こんなことをせずとも TaskCompletionSource を使えばいいです。