有时,我需要在放弃之前将一个操作重试几次。我的代码是:

int retries = 3;
while(true) {
  try {
    DoSomething();
    break; // success!
  } catch {
    if(--retries == 0) throw;
    else Thread.Sleep(1000);
  }
}

我想在一个通用的重试函数中重写这个:

TryThreeTimes(DoSomething);

这在c#中可行吗?TryThreeTimes()方法的代码是什么?


当前回答

下面是一个async/await版本,它可以聚合异常并支持取消。

/// <seealso href="https://learn.microsoft.com/en-us/azure/architecture/patterns/retry"/>
protected static async Task<T> DoWithRetry<T>( Func<Task<T>> action, CancellationToken cancelToken, int maxRetries = 3 )
{
    var exceptions = new List<Exception>();

    for ( int retries = 0; !cancelToken.IsCancellationRequested; retries++ )
        try {
            return await action().ConfigureAwait( false );
        } catch ( Exception ex ) {
            exceptions.Add( ex );

            if ( retries < maxRetries )
                await Task.Delay( 500, cancelToken ).ConfigureAwait( false ); //ease up a bit
            else
                throw new AggregateException( "Retry limit reached", exceptions );
        }

    exceptions.Add( new OperationCanceledException( cancelToken ) );
    throw new AggregateException( "Retry loop was canceled", exceptions );
}

其他回答

以最新的方式实现了LBushkin的答案:

    public static async Task Do(Func<Task> task, TimeSpan retryInterval, int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();
        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    await Task.Delay(retryInterval);
                }

                await task();
                return;
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }

    public static async Task<T> Do<T>(Func<Task<T>> task, TimeSpan retryInterval, int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();
        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    await Task.Delay(retryInterval);
                }
                return await task();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }  

要使用它:

await Retry.Do([TaskFunction], retryInterval, retryAttempts);

而函数[TaskFunction]可以是Task<T>,也可以只是Task。

我需要传递一些参数给我的方法来重试,并有一个结果值;所以我需要一个表达。 我建立了这个类来做这个工作(它是受布什金的启发) 你可以这样使用它:

static void Main(string[] args)
{
    // one shot
    var res = Retry<string>.Do(() => retryThis("try"), 4, TimeSpan.FromSeconds(2), fix);

    // delayed execute
    var retry = new Retry<string>(() => retryThis("try"), 4, TimeSpan.FromSeconds(2), fix);
    var res2 = retry.Execute();
}

static void fix()
{
    Console.WriteLine("oh, no! Fix and retry!!!");
}

static string retryThis(string tryThis)
{
    Console.WriteLine("Let's try!!!");
    throw new Exception(tryThis);
}

public class Retry<TResult>
{
    Expression<Func<TResult>> _Method;
    int _NumRetries;
    TimeSpan _RetryTimeout;
    Action _OnFailureAction;

    public Retry(Expression<Func<TResult>> method, int numRetries, TimeSpan retryTimeout, Action onFailureAction)
    {
        _Method = method;
        _NumRetries = numRetries;
        _OnFailureAction = onFailureAction;
        _RetryTimeout = retryTimeout;
    }

    public TResult Execute()
    {
        TResult result = default(TResult);
        while (_NumRetries > 0)
        {
            try
            {
                result = _Method.Compile()();
                break;
            }
            catch
            {
                _OnFailureAction();
                _NumRetries--;
                if (_NumRetries <= 0) throw; // improved to avoid silent failure
                Thread.Sleep(_RetryTimeout);
            }
        }
        return result;
    }

    public static TResult Do(Expression<Func<TResult>> method, int numRetries, TimeSpan retryTimeout, Action onFailureAction)
    {
        var retry = new Retry<TResult>(method, numRetries, retryTimeout, onFailureAction);
        return retry.Execute();
    }
}

ps。 b布什金的解再重试一次=D

您还可以考虑添加要重试的异常类型。例如,这是您想要重试的超时异常吗?数据库异常?

RetryForExcpetionType(DoSomething, typeof(TimeoutException), 5, 1000);

public static void RetryForExcpetionType(Action action, Type retryOnExceptionType, int numRetries, int retryTimeout)
{
    if (action == null)
        throw new ArgumentNullException("action");
    if (retryOnExceptionType == null)
        throw new ArgumentNullException("retryOnExceptionType");
    while (true)
    {
        try
        {
            action();
            return;
        }
        catch(Exception e)
        {
            if (--numRetries <= 0 || !retryOnExceptionType.IsAssignableFrom(e.GetType()))
                throw;

            if (retryTimeout > 0)
                System.Threading.Thread.Sleep(retryTimeout);
        }
    }
}

您可能还注意到,所有其他示例在测试retries == 0时都存在类似的问题,要么重试无穷大,要么在给定负值时无法引发异常。Sleep(-1000)在上面的catch块中也会失败。这取决于你期望人们有多“愚蠢”,但防御性编程永远不会伤害到你。

在之前工作的基础上,我考虑用三种方式增强重试逻辑:

Specifying what exception type to catch/retry. This is the primary enhacement as retrying for any exception is just plain wrong. Not nesting the last try in a try/catch, achieving slightly better performance Making it an Action extension method static class ActionExtensions { public static void InvokeAndRetryOnException<T> (this Action action, int retries, TimeSpan retryDelay) where T : Exception { if (action == null) throw new ArgumentNullException("action"); while( retries-- > 0 ) { try { action( ); return; } catch (T) { Thread.Sleep( retryDelay ); } } action( ); } }

然后可以像这样调用方法(当然也可以使用匿名方法):

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );

我知道这个答案很老了,但我只是想对此进行评论,因为我在使用这些while, do, whatever语句时遇到了问题。

多年来,我认为我找到了一个更好的方法。这就是使用某种事件聚合,如响应式扩展“Subject”或类似的。当尝试失败时,只需发布一个事件,说明尝试失败,并让聚合器函数重新调度该事件。这使您可以更好地控制重试,而不会用一堆重试循环或其他东西污染调用本身。你也不是用一堆线程来捆绑一个线程。