取消不接受取消令牌的异步操作的正确方法是什么?
本文关键字:取消 方法 是什么 异步操作 不接受 令牌 | 更新日期: 2023-09-27 17:56:05
取消以下内容的正确方法是什么?
var tcpListener = new TcpListener(connection);
tcpListener.Start();
var client = await tcpListener.AcceptTcpClientAsync();
简单地调用tcpListener.Stop()
似乎会导致ObjectDisposedException
,并且AcceptTcpClientAsync
方法不接受CancellationToken
结构。
我完全错过了一些明显的东西吗?
假设您不想在TcpListener
类上调用Stop
方法,这里没有完美的解决方案。
在特定时间范围内完成时收到通知,但允许原始操作完成,则可以创建扩展方法,如下所示:
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
方法的过载需要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
方法就可以了。 这里的优点是它的代码更干净,更易于维护。
虽然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
。