【问题标题】:Equivalent of ContinueWith(delegate, CancellationToken) with await continuationContinueWith(delegate, CancellationToken) 等价于等待继续
【发布时间】:2014-01-27 20:28:19
【问题描述】:

我有这种情况:

private Task LongRunningTask = /* Something */;

private void DoSomethingMore(Task previousTask) { }

public Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
{
    return LongRunningTask.ContinueWith(DoSomethingMore, cancellationToken);
}

特别是,我在这里感兴趣的行为在MSDN's page about Continuation Tasks 中有以下详细说明:

在这些情况下,延续进入Canceled 状态:

上面的代码有效。但是,我正在将尽可能多的延续转换为使用await 关键字。

是否有使用 await 的等效项,可以在等待的任务完成之前取消继续?

【问题讨论】:

  • 好吧,我只是手动检查取消令牌,无论如何都会发生这种情况。也就是说,if (!cancelled) await Task(); if (!cancelled) await Task2(); ... 当然,您也可以将令牌传递给方法(也可以按照您想要的任何方式处理它)。
  • @Luaan:您的评论在概念上与Francois Nel's answer 相同,这并不能解决我的问题。
  • 您确定在使用 ContinueWith 时它的工作方式有什么不同吗?我认为您也只能在任务之间取消,并且通过任务源代码搜索似乎支持这一点 - 无法取消任务本身(除非您自己在任务中处理取消),因为这完全取决于合作多任务,而不是先发制人。在某些时候,Task 类只是启动了您的委托,并且在您返回之前它不能做任何事情。你有没有试过awaitContinueWith取消有区别吗?
  • 例如,您可以通过 await client.GetAsync(uri, cancellationToken); 来等待 HttpClient 并取消。因此,除非Action 委托本身(在托管代码之外“实现”,所以我不能肯定地说)或 ExecutionContext 中存在一些隐藏的魔法,否则您必须在您的方法中“手动”支持取消 - 通过有一个您照常使用的取消令牌参数。
  • 另外,不要忘记awaitasync 使用相同的Task.ContinueWith 方法来执行延续,因此没有理由相信它会以明显不同的方式工作。另请参阅 - msdn.microsoft.com/en-us/library/dd997364.aspxmicrosoft.com/en-us/download/details.aspx?id=19957

标签: c# .net async-await


【解决方案1】:

这个答案来自@Servy 来自this answer(有修改):

public static Task WithCancellation(this Task task,
CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

public static Task<T> WithCancellation<T>(this Task<T> task,
CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

【讨论】:

    【解决方案2】:

    我的回答与@Jean Hominal 的回答略有不同,并且也包含了@Noseratio 的方法:

    public static class TaskExtensionMethods
    {
        public static Task<TResult> OrWhenCancelled<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
        {
            if (!cancellationToken.CanBeCanceled)
                return mainTask;
    
            return OrWhenCancelled_(mainTask, cancellationToken);
        }
    
        private static async Task<TResult> OrWhenCancelled_<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
        {
            Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
            await Task.WhenAny(mainTask, cancellationTask).ConfigureAwait(false);
    
            cancellationToken.ThrowIfCancellationRequested();
            return await mainTask;
        }
    
        public static Task OrWhenCancelled(this Task mainTask, CancellationToken cancellationToken)
        {
            if (!cancellationToken.CanBeCanceled)
                return mainTask;
    
            return OrWhenCancelled_(mainTask, cancellationToken);
        }
    
        private static async Task OrWhenCancelled_(this Task mainTask, CancellationToken cancellationToken)
        {
            Task cancellationTask = Task.Delay(Timeout.Infinite, cancellationToken);
            await Task.WhenAny(mainTask, cancellationTask).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            await mainTask;
        }
    }
    

    讨论:

    • 所有解决方案(包括这个),都没有正确处理原始ContinueWith 指定TaskScheduler 的情况。具体来说,考虑一个为在 UI 场景中使用而创建的 TaskScheduler TaskScheduler.FromCurrentSynchronizationContext。在这种情况下,使用原始的ContinueWith 方法可以保证在运行委托之前但已经进入主线程之后检查取消令牌(请参阅this answer)。也就是说,旧方法具有在考虑任务结果之前在主线程上“最后一次”检查取消标记的良好效果(即胜过主任务是完成还是出错)。这意味着除了使用这些扩展方法之外,新代码还必须将其 await 包装在 try/finally 中,以对 CancellationToken 进行最终检查 :(。参见 this question

    • @Noseratio 的解决方案可以处理上述问题(如果需要),但它的缺点是需要将延续放置到委托中。在我看来,这破坏了转换为使用 await 的一大优势:代码不会以委托结束,它就在 await 之后,并且读起来像普通的顺序代码。

    注意事项:

    • 我希望我可以指定空 lambda 永远不会运行(即,而不是仅在取消时运行),但 .ContinueWith 方法不允许这样做。所以,我(大多是随意选择了 OnlyOnCancelled)

    【讨论】:

    • 嗨,马特,我以后可能会有更多想法,到目前为止是一件事。您认为Task cancellationTask = mainTask.ContinueWith(t =&gt; { }, cancellationToken, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.Default); 可以简单地替换为Task.Delay(Timeout.Infinite, cancellationToken) 吗?后者由 CLR 非常有效地实现。
    • @Noseratio,我尝试了Task.Delay 方法,效果很好。我发现它比目前所有的方法都更具可读性。编辑了我的答案。谢谢!
    • Matt,如果mainTask总是完成,为什么还要使用mainTask.ConfigureAwait(false)? :) 我同意LazyCancellation,但现在我觉得我们都在这里重新发明轮子,正如斯蒂芬图布已经做到的那样:) "How do I cancel non-cancelable async operations?"
    • @Noseratio,啊,是的,ConfigureAwait(false) 是多余的(我将编辑并删除)。 是的我不知道那篇文章。他的方法与我们的方法之间唯一的行为差异是,在任务完成和取消令牌大约同时完成的情况下,我们的解决方案更倾向于兑现取消。我还是更喜欢 Task.Delay 的可读性。而且我真的希望有一个带 CancellationToken 的 ConfigureAwait,因为它会使这种类型的代码的转换具有完全等价的效果。
    • @Noseratio,看看这个版本的 WithCancellation:stackoverflow.com/a/26305788/495262
    【解决方案3】:

    虽然这个答案在概念上与 Noseratio 的答案相同,但我对实现的一些细节并不满意,因此我发布了我提议的帮助器实现,以便其他人可以就这个问题发表评论。

    public static async Task<TResult> WhenNotCanceled<TResult>(this Task<TResult> mainTask, CancellationToken cancellationToken)
    {
        if (!cancellationToken.CanBeCanceled) {
            return await mainTask.ConfigureAwait(false);
        }
    
        cancellationToken.ThrowIfCancellationRequested();
    
        Task<TResult> completedTask;
    
        var cancellationTaskSource = new TaskCompletionSource<TResult>();
        using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
            completedTask = await Task.WhenAny(mainTask, cancellationTaskSource.Task).ConfigureAwait(false);
    
        cancellationToken.ThrowIfCancellationRequested();
        return await completedTask.ConfigureAwait(false);
    }
    
    public static async Task WhenNotCanceled(this Task mainTask, CancellationToken cancellationToken)
    {
        if (!cancellationToken.CanBeCanceled) {
            await mainTask.ConfigureAwait(false);
            return;
        }
    
        cancellationToken.ThrowIfCancellationRequested();
    
        Task completedTask;
    
        var cancellationTaskSource = new TaskCompletionSource<object>();
        using (cancellationToken.Register(() => cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
            completedTask = await Task.WhenAny(mainTask, cancellationTaskSource.Task).ConfigureAwait(false);
    
        cancellationToken.ThrowIfCancellationRequested();
        await completedTask.ConfigureAwait(false);
    }
    

    没有取消的异步模式:

    public async Task IndependentlyCancelableSuccessorTask()
    {
        await LongRunningTask;
        DoSomethingMore();
    }
    

    带有取消和WhenNotCanceled的异步模式:

    public async Task IndependentlyCancelableSuccessorTask(CancellationToken cancellationToken)
    {
        await LongRunningTask.WhenNotCanceled(cancellationToken);
        DoSomethingMore();
    }
    

    【讨论】:

    • 如果您担心捕获同步上下文,我建议您使用using (cancellationToken.Register(() =&gt; cancellationTaskSource.TrySetCanceled(), useSynchronizationContext: false)
    • @Noseratio:我当然关心捕获同步上下文 - 对我来说,从函数返回的 Task 必须能够同步和异步等待 - 如果它被等待同步,那么实际上是调用者的同步上下文被捕获,如果在UI线程上运行很容易触发死锁。此外,回到同步上下文确实是有代价的,为什么在不需要的时候付出呢?
    • 关于您的代码的另一点:它可能将内部TaskCompletionSource.Task暴露给外部调用者。当await completedTask 抛出(如果被取消)时,它在Exception 上可用。 IMO,它应该对模式保持私有。另外,我相信第二个await completedTask 是多余的,正如我在回答的 cmets 中所讨论的那样。在我的版本中,这是 token.ThrowIfCancellationRequested() 的工作。所以,我冒昧地对此投反对票。
    • @Noseratio:我猜你是对的,取消任务不应该外泄。对该修改进行了编辑,并添加了目标使用模式,即我希望具有与 await 相同的行为,其中主任务内部的故障和取消传播到外部。
    • 如果那是你的目标,那么代码就会做它应该做的事情。老板发号施令,+1 :)
    【解决方案4】:

    下面的应该可以,虽然看起来有点别扭:

    private Task LongRunningTask = /* Something */;
    
    private void DoSomethingMore() { }
    
    public async Task IndependentlyCancelableSuccessorTask(
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
    
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(() => tcs.TrySetCanceled()))
            await Task.WhenAny(LongRunningTask, tcs.Task);
    
        cancellationToken.ThrowIfCancellationRequested();
        DoSomethingMore();
    }
    

    [UPDATE] 按照 svick 的建议,这里根据 Stephen Toub 的 Implementing Then with Await 模式将其塑造为助手:

    public static class TaskExt
    {
        /// <summary>
        /// Use: await LongRunningTask.Then(DoSomethingMore, cancellationToken)
        /// </summary>
        public static async Task Then(
            this Task antecedent, Action continuation, CancellationToken token)
        {
            await antecedent.When(token);
            continuation();
        }
    
        /// <summary>
        /// Use: await LongRunningTask.When(cancellationToken)
        /// </summary>
        public static async Task When(
            this Task antecedent, CancellationToken token)
        {
            token.ThrowIfCancellationRequested();
    
            var tcs = new TaskCompletionSource<Empty>();
            using (token.Register(() => tcs.TrySetCanceled()))
                await Task.WhenAny(antecedent, tcs.Task);
    
            token.ThrowIfCancellationRequested();
        }
    
        struct Empty { };
    }
    

    也许,第一个ThrowIfCancellationRequested() 是多余的,但我没有彻底考虑所有边缘情况。

    【讨论】:

    • 我认为您应该以某种方式将其封装到辅助方法中。这样,实际的代码就不会显得尴尬。
    • @svick,我已将其重新分解为助手,这是您的意思吗?
    • @JeanHominal,没问题。很高兴你喜欢这个概念,虽然显然我需要更多的迭代才能满足你的所有要求:)顺便说一句,我喜欢struct EmptyTaskCompletionSource,它是不是我邀请的。
    • @Noseratio: 另外,现在我想我理解了Empty 结构的作用,也就是说,接收到Task 的人不能将其转换为Task&lt;Empty&gt;,因为Empty 不是可见。
    • @Noseratio,我添加了一个答案(这是你的一个分支)。我很想听听您的任何 cmets,因为我尊重您的判断。
    猜你喜欢
    • 2016-08-16
    • 2020-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-05
    • 2014-02-21
    • 1970-01-01
    相关资源
    最近更新 更多