web-dev-qa-db-ja.com

CancellationTokenを受け入れない非同期操作をキャンセルする正しい方法は何ですか?

以下をキャンセルする正しい方法は何ですか?

_var tcpListener = new TcpListener(connection);
tcpListener.Start();
var client = await tcpListener.AcceptTcpClientAsync();
_

単にtcpListener.Stop()を呼び出すとObjectDisposedExceptionが生成され、AcceptTcpClientAsyncメソッドはCancellationToken構造体を受け入れません。

私は明らかな何かを完全に欠いていますか?

28
Jeff

StopクラスTcpListenerメソッド を呼び出さない場合、ここでは完全な解決策はありません。

操作が特定の時間枠内に完了しないときに通知を受けても、元の操作を完了できる場合は、次のような拡張メソッドを作成できます。

public static async Task<T> WithWaitCancellation<T>( 
    this Task<T> task, CancellationToken cancellationToken) 
{
    // The tasck completion source. 
    var tcs = new TaskCompletionSource<bool>(); 

    // Register with the cancellation token.
    using(cancellationToken.Register( s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs) ) 
    {
        // If the task waited on is the cancellation token...
        if (task != await Task.WhenAny(task, tcs.Task)) 
            throw new OperationCanceledException(cancellationToken); 
    }

    // Wait for one or the other to complete.
    return await task; 
}

上記は Stephen Toubのブログ投稿「キャンセルできない非同期操作をキャンセルするにはどうすればよいですか?」 からのものです。

AcceptTcpClientAsync method のオーバーロードがないため、これは実際にはcancel操作ではありません。 CancellationToken 、キャンセルできませんable

つまり、拡張メソッドがキャンセルdidが発生したことを示している場合、元の Taskのコールバックの待機をキャンセルしています。 not操作自体をキャンセルします。

そのために、メソッドの名前をWithCancellationからWithWaitCancellationに変更して、実際のアクションではなくwaitをキャンセルしていることを示しています。

そこから、コードで簡単に使用できます。

// Create the listener.
var tcpListener = new TcpListener(connection);

// Start.
tcpListener.Start();

// The CancellationToken.
var cancellationToken = ...;

// Have to wait on an OperationCanceledException
// to see if it was cancelled.
try
{
    // Wait for the client, with the ability to cancel
    // the *wait*.
    var client = await tcpListener.AcceptTcpClientAsync().
        WithWaitCancellation(cancellationToken);
}
catch (AggregateException ae)
{
    // Async exceptions are wrapped in
    // an AggregateException, so you have to
    // look here as well.
}
catch (OperationCancelledException oce)
{
    // The operation was cancelled, branch
    // code here.
}

待機がキャンセルされた場合にスローされる OperationCanceledException インスタンスをキャプチャするには、クライアントの呼び出しをラップする必要があることに注意してください。

非同期操作からスローされると例外がラップされるため、 AggregateException キャッチでもスローしました(この場合は、自分でテストする必要があります)。

Stop method (基本的に、何が起こっているかにかかわらず、すべてを激しく破壊するものはすべて、 )、もちろん、これはあなたの状況に依存します。

待機しているリソース(この場合はTcpListener)を共有していない場合は、リソースをより適切に使用して、abortメソッドを呼び出し、待機している操作から発生する例外をすべて飲み込んでください。 (stopを呼び出すときにビットを反転し、操作を待機している他の領域でそのビットを監視する必要があります)。これによりコードが多少複雑になりますが、リソースの使用とクリーンアップをできるだけ早く心配する必要があり、この選択肢を利用できる場合は、これが適しています。

リソース使用率がnot問題であり、より協調的なメカニズムに慣れていて、リソースをnot共有している場合は、WithWaitCancellationメソッドを使用するのが適切です。ここでの長所は、コードがよりクリーンで、保守が容易であることです。

22
casperOne

CasperOneの答えは正しいですが、同じ目的を達成するWithCancellation(またはWithWaitCancellation)拡張メソッドには、よりクリーンな潜在的な実装があります。

static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}
  • 最初に、タスクが既に完了しているかどうかを確認することにより、高速パスの最適化を行います。
  • 次に、継続を元のタスクに登録し、CancellationTokenパラメーターを渡します。
  • 継続は、可能であれば、元のタスクの結果(または、存在する場合は例外)を同期的に抽出します(TaskContinuationOptions.ExecuteSynchronously)、そうでない場合はThreadPoolスレッドを使用します(TaskScheduler.Default)キャンセルのためにCancellationTokenを監視しているとき。

CancellationTokenがキャンセルされる前に元のタスクが完了すると、返されたタスクが結果を保存します。それ以外の場合、タスクはキャンセルされ、待機するとTaskCancelledExceptionがスローされます。

11
i3arnon