15

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

4

3 回答 3

41

更新 5/2017

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

原来的

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

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

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);
    }
于 2012-05-08T07:28:20.860 回答
5

这是我已经测试并在生产中使用的Panagiotis Kanavos 出色答案的重复版本。

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

  • 希望能够根据先前尝试的次数和当前尝试的异常来决定是否重试
  • 不想依赖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;
}
于 2013-05-03T08:15:49.707 回答
2

在高层次上,我发现根据您拥有的和想要的进行功能签名会很有帮助。

你有:

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

你要:

  • 总体任务

因此,您将拥有如下功能:

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

扩展函数内部的实践,任务几乎有 2 个操作与它们有关,读取它们的状态和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;
于 2012-05-08T00:52:21.097 回答