【问题标题】:What is the correct way to cancel an async operation that doesn't accept a CancellationToken?取消不接受 CancellationToken 的异步操作的正确方法是什么?
【发布时间】:2013-01-25 14:49:43
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: c# .net asynchronous task-parallel-library async-await


    【解决方案1】:

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

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

    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's blog post "How do I cancel non-cancelable async operations?"

    这里的警告需要重复,这实际上并没有取消操作,因为没有AcceptTcpClientAsync method 的重载需要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 method 这样的方法(基本上,无论发生什么,任何猛烈撕裂一切的东西),哪种方法更好,这当然取决于你的情况。

    如果您不共享您正在等待的资源(在本例中为 TcpListener),那么调用 abort 方法并吞下来自操作的任何异常可能会更好地利用资源您正在等待(当您调用 stop 并在您等待操作的其他区域中监视该位时,您必须稍微翻转一下)。这会增加代码的一些复杂性,但如果您担心资源利用率和尽快清理,并且您可以选择此选项,那么这就是要走的路。

    如果资源利用率不是问题,并且您对更协作的机制感到满意,并且您共享资源,那么使用WithWaitCancellation方法很好。这里的优点是它的代码更简洁,更易于维护。

    【讨论】:

    • 这一行有什么区别: using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true) , tcs)) 和我写它的方式(原因上一个没看懂): using (cancellationToken.Register(() => tcs.TrySetResult(true))) ?
    【解决方案2】:

    虽然 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

    【讨论】:

    • 你为什么用completedTask.GetAwaiter().GetResult()而不是completedTask.Result
    • @GlennSlayden 如果有一个与 Task.Result 相反的抛出 AggregateException 的异常,它会抛出第一个(即“真实”)异常
    • 好的,太好了;谢谢。您还想将cancellationToken.ThrowIfCancellationRequested(); 添加为有用的扩展方法的第一行吗?
    • @GlennSlayden 你可以......虽然它不是真的需要。续展将立即取消。此外,消费者可能会对同步抛出的异常而不是等待任务感到惊讶。
    • @i3arnon ContinueWith 的 cancelToken 参数描述为“将分配给新的延续任务的 CancellationToken”。您确定这实际上会在原始任务完成之前“提前”取消吗?文档另有说明
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多