【问题标题】:Retry a task multiple times based on user input in case of an exception in task任务发生异常时,根据用户输入多次重试任务
【发布时间】:2012-05-16 10:42:54
【问题描述】:

我的应用程序中的所有服务调用都是作为任务实现的。当某个任务出现故障时,我需要向用户显示一个对话框以重试上次失败的操作。如果用户选择重试,程序应该重试该任务,否则程序的执行应该在记录异常后继续。任何人对如何实现这个功能有一个高级的想法?

【问题讨论】:

  • @svick 我还没有尝试实现这个功能,这是一个未来的任务,看起来我们已经有了有趣的想法,必须详细了解它们并尝试一下

标签: c# wpf exception-handling task-parallel-library task


【解决方案1】:

2017 年 5 月更新

C# 6 异常过滤器使catch 子句变得更加简单:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch when (retryCount-- > 0){}
        }
    }

还有一个递归版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch when (retryCount-- > 0){}
        return await Retry(func, retryCount);
    }

原创

编写重试函数的方法有很多种:您可以使用递归或任务迭代。不久前,希腊 .NET 用户组中有一个 discussion 讨论了执行此操作的不同方法。
如果您使用 F#,您还可以使用 Async 构造。不幸的是,您至少不能在 Async CTP 中使用 async/await 构造,因为编译器生成的代码不喜欢多个等待或可能在 catch 块中重新抛出。

递归版本可能是在 C# 中构建重试的最简单方法。以下版本不使用 Unwrap 并在重试之前添加可选延迟:

private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    Task.Factory.StartNewDelayed(delay).ContinueWith(t =>
                    {
                        Retry(func, retryCount - 1, delay,tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

StartNewDelayed 函数来自ParallelExtensionsExtras 样本,并在超时发生时使用计时器触发 TaskCompletionSource。

F# 版本要简单得多:

let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = 
let rec retry' retryCount = 
    async {
        try
            let! result = asyncComputation  
            return result
        with exn ->
            if retryCount = 0 then
                return raise exn
            else
                return! retry' (retryCount - 1)
    }
retry' retryCount

不幸的是,在 C# 中使用 Async CTP 中的 async/await 编写类似的东西是不可能的,因为编译器不喜欢 catch 块中的 await 语句。以下尝试也静默失败,因为运行时不喜欢在异常后遇到等待:

private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await TaskEx.Run(func);
                return result;
            }
            catch 
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

关于询问用户,可以修改Retry,调用一个询问用户并通过TaskCompletionSource返回任务的函数,在用户回答时触发下一步,例如:

 private static Task<bool> AskUser()
    {
        var tcs = new TaskCompletionSource<bool>();
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine(@"Error Occured, continue? Y\N");
            var response = Console.ReadKey();
            tcs.SetResult(response.KeyChar=='y');

        });
        return tcs.Task;
    }

    private static Task<T> RetryAsk<T>(Func<T> func, int retryCount,  TaskCompletionSource<T> tcs = null)
    {
        if (tcs == null)
            tcs = new TaskCompletionSource<T>();
        Task.Factory.StartNew(func).ContinueWith(_original =>
        {
            if (_original.IsFaulted)
            {
                if (retryCount == 0)
                    tcs.SetException(_original.Exception.InnerExceptions);
                else
                    AskUser().ContinueWith(t =>
                    {
                        if (t.Result)
                            RetryAsk(func, retryCount - 1, tcs);
                    });
            }
            else
                tcs.SetResult(_original.Result);
        });
        return tcs.Task;
    } 

通过所有的延续,您可以看到为什么异步版本的 Retry 如此受欢迎。

更新:

在 Visual Studio 2012 Beta 中,以下两个版本有效:

带有while循环的版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        while (true)
        {
            try
            {
                var result = await Task.Run(func);
                return result;
            }
            catch
            {
                if (retryCount == 0)
                    throw;
                retryCount--;
            }
        }
    }

还有一个递归版本:

    private static async Task<T> Retry<T>(Func<T> func, int retryCount)
    {
        try
        {
            var result = await Task.Run(func);
            return result;
        }
        catch
        {
            if (retryCount == 0)
                throw;
        }
        return await Retry(func, --retryCount);
    }

【讨论】:

  • 感谢您的详细解释,我会尝试并让您知道
【解决方案2】:

这是我已经测试并在生产中使用的 Panagiotis Kanavos's excellent answer 的 riffed 版本。

它解决了一些对我来说很重要的事情:

  • 希望能够根据先前尝试的次数和当前尝试的异常来决定是否重试
  • 不想依赖async(环境限制较少)
  • 希望在失败的情况下生成的 Exception 包含每次尝试的详细信息


static Task<T> RetryWhile<T>(
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry )
{
    return RetryWhile<T>( func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>() );
}

static Task<T> RetryWhile<T>( 
    Func<int, Task<T>> func, 
    Func<Exception, int, bool> shouldRetry, 
    TaskCompletionSource<T> tcs, 
    int previousAttempts, IEnumerable<Exception> previousExceptions )
{
    func( previousAttempts ).ContinueWith( antecedent =>
    {
        if ( antecedent.IsFaulted )
        {
            var antecedentException = antecedent.Exception;
            var allSoFar = previousExceptions
                .Concat( antecedentException.Flatten().InnerExceptions );
            if ( shouldRetry( antecedentException, previousAttempts ) )
                RetryWhile( func,shouldRetry,previousAttempts+1, tcs, allSoFar);
            else
                tcs.SetException( allLoggedExceptions );
        }
        else
            tcs.SetResult( antecedent.Result );
    }, TaskContinuationOptions.ExecuteSynchronously );
    return tcs.Task;
}

【讨论】:

    【解决方案3】:

    在高级别的时候,我发现根据你拥有的和想要的来制作函数签名会很有帮助。

    你有:

    • 为您提供任务的函数 (Func&lt;Task&gt;)。我们将使用该函数,因为任务本身通常不可重试。
    • 确定整个任务是否已完成或应重试的函数 (Func&lt;Task, bool&gt;)

    你想要:

    • 总体任务

    所以你会有这样的功能:

    Task Retry(Func<Task> action, Func<Task, bool> shouldRetry);
    

    扩展函数内部的实践,任务几乎有两个操作要做,读取它们的状态和ContinueWith。要制作自己的任务,TaskCompletionSource 是一个很好的起点。第一次尝试可能类似于:

    //error checking
    var result = new TaskCompletionSource<object>();
    action().ContinueWith((t) => 
      {
        if (shouldRetry(t))
            action();
        else
        {
            if (t.IsFaulted)
                result.TrySetException(t.Exception);
            //and similar for Canceled and RunToCompletion
        }
      });
    

    这里明显的问题是只会发生 1 次重试。为了解决这个问题,您需要为函数提供一种调用自身的方法。使用 lambdas 执行此操作的通常方法是这样的:

    //error checking
    var result = new TaskCompletionSource<object>();
    
    Func<Task, Task> retryRec = null; //declare, then assign
    retryRec = (t) => { if (shouldRetry(t))
                            return action().ContinueWith(retryRec).Unwrap();
                        else
                        {
                            if (t.IsFaulted) 
                                result.TrySetException(t.Exception);
                            //and so on
                            return result.Task; //need to return something
                         }
                      };
     action().ContinueWith(retryRec);
     return result.Task;
    

    【讨论】:

    • 谢谢,会尽力让你知道
    猜你喜欢
    • 1970-01-01
    • 2019-01-11
    • 2020-11-05
    • 2022-10-21
    • 2012-01-16
    • 2021-05-04
    • 2011-10-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多