在任务中发生异常的情况下,根据用户输入多次重试任务

Vas*_*nan 13 c# wpf exception-handling task task-parallel-library

在我的应用程序中的所有服务调用实现为tasks.When永远的任务发生故障,我需要一个对话框,向用户呈现failed.If用户选择重试程序应该重试任务重试最后一次操作,否则记录异常后,程序的执行应该继续.任何人都对如何实现这个功能有了很高的想法?

Pan*_*vos 36

更新时间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){}
        }
    }
Run Code Online (Sandbox Code Playgroud)

和递归版本:

    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);
    }
Run Code Online (Sandbox Code Playgroud)

原版的

编写重试函数的方法有很多种:您可以使用递归或任务迭代.有一个讨论希腊.NET用户组中的不同的方式做的正是这种而回.
如果您使用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;
    } 
Run Code Online (Sandbox Code Playgroud)

所述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
Run Code Online (Sandbox Code Playgroud)

不幸的是,不可能在Async CTP中使用async/await在C#中编写类似的东西,因为编译器不喜欢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--;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

至于询问用户,您可以修改重试以调用询问用户的函数,并通过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;
    } 
Run Code Online (Sandbox Code Playgroud)

通过所有延续,您可以了解为什么异步版本的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--;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

和递归版本:

    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);
    }
Run Code Online (Sandbox Code Playgroud)


Rub*_*ink 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;
}
Run Code Online (Sandbox Code Playgroud)