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/"));
一方で、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での待機が終了し、パラメーターで渡していた値が結果として返されます。
エラーが発生したり、キャンセルが発生したときは、それぞれTrySetException、TrySetCanceledを呼び出せばOKです。
余談ですが、私がこれを欲しくなったのは、SerialPortクラスを触っているときでした。
そもそもシリアルポートは単なる全二重通信ですから、本来はWebClientのように「データを要求して、そのレスポンスが返ってくる」という使い方に限定されるようなものではありません。しかし、「何かしらのコマンドを送るとそれに対応したレスポンスがある」のような実装を行うシチュエーションはそれなりにあり、そのような場合ではTAPでクライアントを作ったほうがスッキリします。
というわけで、SerialPortクラスをTAPでラッピングしようとしたときに使ったのがこの手法でした。
0 件のコメント:
コメントを投稿