2018年1月13日土曜日

eventをasync/awaitに変換する

今回のテーマはいわゆるEAPからTAPへの変換です。

Webアクセスなど、長い時間がかかることが見込まれる処理では、あるスレッドがそれにつきっきりになるとほかの処理が一切できず、ユーザーからはソフトがハングアップ(フリーズ)しているように見えてしまいます。そのような状態を防ぐために、非同期処理が必要になってきます。

EAP(Event-based Asynchronous Pattern)は、非同期プログラミングにおいて、処理の開始を行うメソッドと、終了時に呼ばれるイベントからなるパターンです。
例えば、WebClientクラスはこのパターンをサポートしており、Webからのダウンロードを非同期で実行することができます。
WebClient wc = new WebClient();

wc.DownloadStringCompleted += (s, e) => {
    Console.WriteLine(e.Result);
};

wc.DownloadStringAsync(new Uri("https://www.google.co.jp/"));
これはGoogleのトップページのソースを画面に表示するプログラムをEAPで実装したものです。イベントを使っているため、ダウンロードを開始する前にダウンロード後の処理を綴ったハンドラをイベントに登録しておき、その後に処理を開始するという書き方をしなければなりません。プログラムが上から下へ流れないので非常に見にくくなってしまいます。

一方で、TAP(Task-based Asynchronous Pattern)はTaskクラス/Task<T>クラスを使用した非同期パターンです。ライブラリとしてはこの説明だけなのですが、C#では言語機能でもこの非同期パターンに追従していて、C#5.0でasync/await構文が導入されました。これは非同期処理を同期処理であるかのような見た目で実行できる構文で、TAPとセットでこの構文を使うことによって非同期処理の記述がかなりすっきりできるようになりました。
WebClientクラスはこのパターンもサポートしており、上記のプログラムはTAPだと次のように書き換えられます。
WebClient wc = new WebClient();

string result = await wc.DownloadStringTaskAsync("https://www.google.co.jp/");
Console.WriteLine(result);
見てくださいこのシンプルさ。文字列をダウンロードして画面に出力する、というシンプルな流れのみを記述していますが、実際ちゃんと非同期で動作します。人類が古くから慣れ親しんでいる同期パターンと同じように非同期なプログラムを記述できる素晴らしい機能です。


さて、復習はこれくらいで本題に入ります。
WebClientのようにTAPに対応してくれているクラスならいいのですが、世の中にはそうではないクラスもたくさんあります。古いライブラリだったり、そもそももうちょっと汎用的なプログラムでEAPと呼ばれるものでは無かったり。

ズバリ、TaskCompletionSource<T>クラスを使います。
static async Task Main(string[] args)
{
    string result = await DownloadStringTaskAsync("https://www.google.co.jp/");
    Console.WriteLine(result);
}

static Task<string> DownloadStringTaskAsync(string url)
{
    var tcs = new TaskCompletionSource<string>();
    var wc = new WebClient();
    wc.DownloadStringCompleted += (sender, e) => {
        if(e.Error != null)
            tcs.TrySetException(e.Error);
        else if(e.Cancelled)
            tcs.TrySetCanceled();
        else
            tcs.TrySetResult(e.Result);

        wc.Dispose();
    };
    wc.DownloadStringAsync(new Uri(url));
    return tcs.Task;
}
結論から言うとこんな感じです。
TaskCompletionSourceクラスにはTaskプロパティがあり、このプロパティをawaitすることで、TaskCompletionSourceの終了待ちをすることができます。
ではどうやったらTaskCompletionSourceが終了するのかというと、TrySetResultが呼ばれたときになります。これを呼び出すことでawaitでの待機が終了し、パラメーターで渡していた値が結果として返されます。
エラーが発生したり、キャンセルが発生したときは、それぞれTrySetExceptionTrySetCanceledを呼び出せばOKです。


余談ですが、私がこれを欲しくなったのは、SerialPortクラスを触っているときでした。
そもそもシリアルポートは単なる全二重通信ですから、本来はWebClientのように「データを要求して、そのレスポンスが返ってくる」という使い方に限定されるようなものではありません。しかし、「何かしらのコマンドを送るとそれに対応したレスポンスがある」のような実装を行うシチュエーションはそれなりにあり、そのような場合ではTAPでクライアントを作ったほうがスッキリします。
というわけで、SerialPortクラスをTAPでラッピングしようとしたときに使ったのがこの手法でした。

0 件のコメント:

コメントを投稿