15

アプリケーションのすべてのサービス呼び出しはタスクとして実装されています。タスクが失敗したときはいつでも、失敗した最後の操作を再試行するためのダイアログ ボックスをユーザーに提示する必要があります。ユーザーが再試行を選択した場合、プログラムはタスクを再試行する必要があります。例外をログに記録した後、プログラムの実行を続行する必要があります。この機能を実装する方法について高レベルのアイデアを持っている人はいますか?

4

3 に答える 3

41

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

オリジナル

Retry 関数をコーディングするには多くの方法があります。再帰またはタスクの反復を使用できます。正確にこれを行うためのさまざまな方法について、しばらく前にギリシャの .NET ユーザー グループで議論がありました。
F# を使用している場合は、Async コンストラクトも使用できます。残念ながら、少なくとも Async CTP では async/await コンストラクトを使用できません。これは、コンパイラによって生成されたコードが複数の await や catch ブロックでの再スローを好まないためです。

再帰バージョンは、おそらく C# で Retry を作成する最も簡単な方法です。次のバージョンでは 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

残念ながら、コンパイラは catch ブロック内の await ステートメントを好まないため、Async CTP から async/await を使用して C# で同様のものを作成することはできません。ランタイムは例外の後に 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 では、次の 2 つのバージョンが機能します。

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 に答える