31

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

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

単に呼び出すだけtcpListener.Stop()で結果が得られるようObjectDisposedExceptionで、メソッドは構造体 AcceptTcpClientAsyncを受け入れません。CancellationToken

私は完全に明白な何かを逃していますか?

4

2 に答える 2

25

クラスの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; 
}

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

ここでの警告は繰り返しになりますが、これは実際には操作をキャンセルしません。これは、をとるAcceptTcpClientAsyncメソッドの過負荷がないため、キャンセルすることができCancellationTokenないためです。

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

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

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

// 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それは、もちろん、あなたの状況に依存する方法のような方法(基本的に、何が起こっているかに関係なく、すべてを激しく破壊するもの)を持っていることに直面して、どちらのアプローチがより良いアプローチであるかという問題を残します。

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

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

于 2013-01-25T15:07:30.260 に答える
14

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を使用して() 、キャンセルを監視します。ThreadPoolTaskScheduler.DefaultCancellationToken

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

于 2014-11-15T05:03:50.333 に答える