31

取消以下的正确方法是什么?

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

简单地调用tcpListener.Stop()似乎会导致一个ObjectDisposedException并且该AcceptTcpClientAsync方法不接受CancellationToken结构。

我完全错过了一些明显的东西吗?

4

2 回答 2

25

假设您不想调用该Stop方法TcpListenerclass上,这里没有完美的解决方案。

如果您可以在操作未在特定时间范围内完成时收到通知,但允许原始操作完成,那么您可以创建一个扩展方法,如下所示:

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 的博文“如何取消不可取消的异步操作?” .

这里需要重复一下,这实际上并没有取消操作,因为没有采用 a的AcceptTcpClientAsync方法CancellationToken的重载,它不能取消。

这意味着,如果扩展方法表明确实发生了取消,则您正在取消对原始回调的等待Task而不是取消操作本身。

为此,这就是为什么我将方法重命名为WithCancellationtoWithWaitCancellation以表明您正在取消等待,而不是实际操作。

从那里,它很容易在您的代码中使用:

// 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(or 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同时观察CancellationTokenfor 取消。

如果原始任务在CancellationToken被取消之前完成,则返回的任务存储结果,否则任务被取消并TaskCancelledException在等待时抛出一个。

于 2014-11-15T05:03:50.333 回答