2012-05-07 67 views
10

我的應用程序中的所有服務調用均作爲任務實現。當有任務出現故障時,我需要向用戶提供一個對話框以重試最後的操作失敗了。如果用戶選擇重試,程序應該重試任務,否則在記錄異常之後程序的執行應該繼續。任何人都已經對如何實現這個功能有了高層次的想法?任務中出現異常時,根據用戶輸入多次重試任務

+0

@svick我沒試過來實現這個功能它是一個未來的任務來了,看起來我們得到了有趣的想法已經,已經通過他們去詳細,並給它一個嘗試 –

回答

31

UPDATE二千零十七分之五

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

ORIGINAL

有很多方法來編寫一個重試功能:您可以使用遞歸或任務迭代。在希臘.NET用戶組中有一個discussion,而回到完成這個的不同方式。
如果你使用F#,你也可以使用Async結構。不幸的是,至少在Async CTP中不能使用異步/等待結構,因爲編譯器生成的代碼不像多個等待或catch塊中可能的重新拋出。

遞歸版本可能是在C#中構建重試的最簡單方法。下面的版本不使用展開和重試前增加了一個可選的延遲:

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#中類似的東西從異步CTP /等待,因爲編譯器不喜歡裏面等待語句一個catch塊。下面的嘗試也失敗silenty,因爲運行時不喜歡異常後遇到的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--; 
      } 
     } 
    } 

至於詢問用戶,你可以修改重試打電話,詢問用戶,並返回任務的功能通過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; 
    } 

與所有的延續,你可以看到爲什麼重試的異步版本是這麼希望的。

UPDATE:

在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); 
    } 
+0

感謝您的詳細解釋,將嘗試這一點,讓你知道 –

+1

+1好的工作。 [在下面添加了一個段落](http://stackoverflow.com/a/16354355/11635) –

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重試將不會發生。爲了解決這個問題,你需要爲函數自己調用一個方法。通常的方式與lambda表達式來做到這一點是這樣的:

//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; 
+0

感謝,將試着讓你知道 –

4

這裏是一個riffed版本Panagiotis Kanavos's excellent answer我已經測試過,並在生產中使用。

它解決一些事情,是很重要的對我說:

  • 希望能夠決定基於目前從前面的嘗試和努力例外數量是否重試
  • 不想依靠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; 
}