Open6

C#の非同期実装

forest1forest1

メソッド内でイベント待ちするパターン

nugetから System.Reactive を追加しておく

using System.Reactive.Linq;

    public partial class Form1 : Form
    {
        private async void button1_Click(object sender, EventArgs e)
        {
            var o = new MyController();

            TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
            using (Observable
                .FromEventPattern<MyEventArgs>(x => o.Received += x, x => o.Received -= x)  // usingの最初にイベントをAdd、抜ける時にRemoveする
                .Select(x => x.EventArgs)   // イベントが呼び出された時に来る。イベントのMyEventArgsを取得
                .Subscribe(x => tcs.SetResult(x.Data))) // TaskCompletionSourceにMyEventArgs.Dataの値を設定
            {
                o.FireEvent("test");  // イベントを発生させる
                var data = await tcs.Task;  // TaskCompletionSource.SetResult()が呼ばれるまで待つ
                // data == "test"
            }
        }
    }
    class MyEventArgs : EventArgs
    {
        public MyEventArgs(string data) => this.Data = data;
        public string Data { get; set; }
    }

    class MyController
    {
        public event EventHandler<MyEventArgs> Received;

        public void FireEvent(string data) => this.Received.Invoke(this, new MyEventArgs(data));
    }
forest1forest1

例えば、MyControllerが通信コンポーネントの場合に。
コマンドを送信(FireEvent("test"))して相手からの応答を受信(Received)するのを待つような場合に使える。

forest1forest1

メソッドを呼び出してイベントハンドラが呼び出されるのを待つようなケースを同じメソッド内で完結させることが出来るのが特徴

forest1forest1

await task; のタスク終了待ちでタイムアウトとかCancellationTokenを指定する方法

public static class TaskEx
{
    // 戻り値無し
    public static Task WaitAsync(this Task task, TimeSpan timeout) => WaitAsync(task, timeout, default);

    public static async Task WaitAsync(this Task task, TimeSpan timeout, CancellationToken token)
    {
        var targetTask = await Task.WhenAny(new Task[]
            {
                task,
                Task.Delay(timeout, token),
            }
        );
        if (targetTask == task)
        {
            await task;  // 普通にtaskが終了
        }
        else
        {
            token.ThrowIfCancellationRequested();  // キャンセルだったらThrow
            throw new TimeoutException();  // でなければタイムアウト
        }
    }

    // 戻り値有り
    public static Task<T> WaitAsync<T>(this Task<T> task, TimeSpan timeout) => WaitAsync<T>(task, timeout, default);
    public static async Task<T> WaitAsync<T>(this Task<T> task, TimeSpan timeout, CancellationToken token)
    {
        var targetTask = await Task.WhenAny(new Task[]
            {
                task,
                Task.Delay(timeout, token),
            }
        );
        if (targetTask == task)
        {
            return await task;  // 普通にtaskが終了
        }
        else
        {
            token.ThrowIfCancellationRequested();  // キャンセルだったらThrow
            throw new TimeoutException();  // でなければタイムアウト
        }
    }
}

使い方

// タイムアウト有り
var result = await task.WaitAsync(TimeSpan.FromSeconds(1000));
// タイムアウト+キャンセル有り
CancellationTokenSource cts = new CancellationTokenSource();
var result = await task.WaitAsync(TimeSpan.FromSeconds(1000), cts.Token);
forest1forest1

もちろんタイムアウトもキャンセルも無いのは、

await task;
forest1forest1

普通はtask内でタイムアウトとかキャンセル処理をすべき
なので、WaitAsync()の使いどころとしては最初のSystem.Reactiveによる、
メソッド内でイベント待ちするパターンでTaskCompletionSourceのタスク待ちで使う

var data = await tcs.Task.WaitAsync(timeout, cts.Token);