与等待继续的ContinueWith(delegate,CancellationToken)的等效

时间:2014-01-09 10:30:28

标签: c# .net async-await

我有这种情况:

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的等效项,允许在等待任务完成之前取消继续?

4 个答案:

答案 0 :(得分:6)

以下应该这样做,虽然看起来有点尴尬:

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();
}

[更新] 根据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()是多余的,但我没有彻底考虑所有边缘情况。

答案 1 :(得分: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();
}

答案 2 :(得分:1)

我的答案与@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。< / p>

  • @ Noseratio的解决方案可以处理上述问题(如果需要),但它有一个缺点,即要求将继续放在委托中。在我看来,这失败了转换为使用await的一大优势:代码不会在委托中结束,它只是在await之后,并且读取像普通的顺序代码。 / p>

注意:

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

答案 3 :(得分:0)

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

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);
}