在异步重试操作中实现超时

Implementing timeout in async retry operations

我写了一个带有重试逻辑的异步方法。它工作得很好,但是最近我想为每次尝试添加一个超时 以防操作时间太长。

public static async Task<Result> PerformAsync(Func<Task> Delegate,
    Func<Exception, bool> FailureCallback = null, int Timeout = 30000,
    int Delay = 1000, int Threshold = 10)
{
    if (Delegate == null)
    {
        throw new ArgumentNullException(nameof(Delegate));
    }

    if (Threshold < 1)
    {
        throw new ArgumentOutOfRangeException(nameof(Threshold));
    }

    CancellationTokenSource Source = new CancellationTokenSource();
    CancellationToken Token = Source.Token;

    bool IsSuccess = false;

    for (int Attempt = 0; Attempt <= Threshold && !Source.IsCancellationRequested;
        Attempt++)
    {
        try
        {
            await Delegate();

            Source.Cancel();

            IsSuccess = true;

            break;
        }

        catch (Exception E)
        {
            Exceptions.Add(E);

            if (FailureCallback != null)
            {
                bool IsCanceled =
                    Application.Current.Dispatcher.Invoke(new Func<bool>(() =>
                {
                    return !FailureCallback(E);
                }));

                if (IsCanceled)
                {
                    Source.Cancel();

                    IsSuccess = false;

                    break;
                }
            }
        }

        await Task.Delay(Delay);
    }

    return new Result(IsSuccess, new AggregateException(Exceptions));
}

我一直在网上尝试各种解决方案,但无论出于何种原因,我从未设法单独为每次尝试设置超时。

我尝试使用 Task.WhenAny()Task.Delay(Timeout) 来做到这一点,但是当我启动我的程序时,FailureCallback 只被调用一次,如果再次尝试失败,FailureCallback未被调用。

好的,让我们开始吧。首先,CancellationToken 的预期用途不是在本地取消循环,那是一种浪费,CancellationToken 保留了一些资源,在您的情况下,您可以简单地使用布尔值。

bool IsSuccess = false;
bool IsCancelled = false;

for (int Attempt = 0; Attempt <= Threshold; Attempt++)
{

    try
    {
        await Delegate();
        IsSuccess = true;
        //You are breaking the for loop, no need to test the boolean
        //in the for conditions
        break;
    }

    catch (Exception E)
    {
        Exceptions.Add(E);

        if (FailureCallback != null)
        {
            IsCancelled = Application.Current.Dispatcher.Invoke(new Func<bool>(() =>
            {
                    return !FailureCallback(E);
            }));

            //You are breaking the for loop, no need to test the boolean
            //in the for conditions

            if(IsCancelled)
                break;

        }
    }

    await Task.Delay(Delay);
}

//Here you have "IsSuccess" and "IsCancelled" to know what happened in the loop
//If IsCancelled is true the operation was cancelled, if IsSuccess is true
//the operation was success, if both are false the attempt surpased threshold.

其次,您必须将您的委托更新为可取消,这是 CancellationToken 的真正预期用途,让您的委托期待 CancellationToken 并在函数内正确使用它。

public static async Task<Result> PerformAsync(Func<CancellationToken, Task> Delegate, //..

//This is an example of the Delegate function
public Task MyDelegateImplemented(CancellationToken Token)
{

    //If you have a loop check if it's cancelled in each iteration
    while(true)
    {
        //Throw a TaskCanceledException if the cancellation has been requested
        Token.ThrowIfCancellationRequested();

        //Now you must propagate the token to any async function
        //that accepts it
        //Let's suppose you are downloading a web page

        HttpClient client;

        //...

        await client.SendAsync(message, Token)

    }

}

最后,既然您的任务是可取消的,您可以像这样实现超时:

//This is the "try" in your loop
try
{
    var tokenSource = new CancellationTokenSource();

    var call = Delegate(tokenSource.Token);
    var delay = Task.Delay(timeout, tokenSource.Token);

    var finishedTask = await Task.WaitAny(new Task[]{ call, delay });

    //Here call has finished or delay has finished, one will
    //still be running so you need to cancel it

    tokenSource.Cancel();
    tokenSource.Dispose();

    //WaitAny will return the task index that has finished
    //so if it's 0 is the call to your function, else it's the timeout

    if(finishedTask == 0)
    {
        IsSuccess = true;
        break;
    }
    else
    {
        //Task has timed out, handle the retry as you need.
    }

}