取消不接受CancellationToken的异步操作的正确方法是什么?

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

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

简单地调用tcpListener.Stop()似乎会导致ObjectDisposedException ,而AcceptTcpClientAsync方法不接受CancellationToken结构。

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

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

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

 public static async Task WithWaitCancellation( this Task task, CancellationToken cancellationToken) { // The tasck completion source. var tcs = new TaskCompletionSource(); // Register with the cancellation token. using(cancellationToken.Register( s => ((TaskCompletionSource)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方法的重载采用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 catch,因为从异步操作中抛出exception(在这种情况下你应该自己测试)。

这就留下了一个问题,即面对一种类似Stop方法的方法 (基本上,任何暴力撕下一切,无论发生什么事情),哪种方法都是更好的方法,当然,这取决于你的情况。

如果您没有共享您正在等待的资源(在本例中为TcpListener ),那么可能更好地利用资源来调用abort方法并吞下来自您正在等待的操作的任何exception(当你调用停止并监视你正在等待某个操作的其他区域中的那个位时,你将不得不翻转一下)。 这会增加代码的复杂性,但如果您担心资源利用率和尽快清理,并且您可以选择此选项,那么这就是您要做的。

如果资源利用率不是问题,并且您对更合作的机制感到满意,并且您没有共享资源,那么使用WithWaitCancellation方法就可以了。 这里的优点是代码更清晰,更易于维护。

虽然casperOne的答案是正确的,但WithCancellation (或WithWaitCancellation )扩展方法有一个更清晰的潜在实现,可实现相同的目标:

 static Task WithCancellation(this Task task, CancellationToken cancellationToken) { return task.IsCompleted ? task : task.ContinueWith( completedTask => completedTask.GetAwaiter().GetResult(), cancellationToken, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } 
  • 首先,我们通过检查任务是否已经完成来进行快速路径优化。
  • 然后我们只需将延续注册到原始任务并传递CancellationToken参数。
  • 如果可能, TaskContinuationOptions.ExecuteSynchronously同步提取原始任务的结果(如果有的话,则为exception)( TaskContinuationOptions.ExecuteSynchronously ),如果不是,则使用ThreadPool线程( TaskScheduler.Default ),同时观察CancellationToken以进行取消。

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