取消不接受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
。