Restarting a task in the background if certain err

2019-01-28 07:50发布

问题:

I am using some REST requests using Mono.Mac (3.2.3) to communicate with a server, and as a retry mechanism I am quietly attempting to give the HTTP actions multiple tries if they fail, or time out.

I have the following;

var tries = 0;
while (tries <= ALLOWED_TRIES)
{
    try
    {
        postTask.Start();
        tries++;
        if (!postTask.Wait(Timeout))
        {
            throw new TimeoutException("Operation timed out");
        }
        break;
    } catch (Exception e) {
        if (tries > ALLOWED_TRIES)
        {
            throw new Exception("Failed to access Resource.", e);
        }
    }
}

Where the task uses parameters of the parent method like so;

var postTask = new Task<HttpWebResponse>(() => {return someStuff(foo, bar);},
    Task.Factory.CancellationToken, 
    Task.Factory.CreationOptions);

The problem seems to be that the task does not want to be run again with postTask.Start() after it's first completion (and subsequent failure). Is there a simple way of doing this, or am I misusing tasks in this way? Is there some sort of method that resets the task to its initial state, or am I better off using a factory of some sort?

回答1:

You're indeed misusing the Task here, for a few reasons:

  • You cannot run the same task more than once. When it's done, it's done.

  • It is not recommended to construct a Task object manually, there's Task.Run and Task.Factory.Start for that.

  • You should not use Task.Run/Task.Factory.Start for a task which does IO-bound work. They are intended for CPU-bound work, as they "borrow" a thread from ThreadPool to execute the task action. Instead, use pure async Task-based APIs for this, which do not need a dedicate thread to complete.

For example, below you can call GetResponseWithRetryAsync from the UI thread and still keep the UI responsive:

async Task<HttpWebResponse> GetResponseWithRetryAsync(string url, int retries)
{
    if (retries < 0)
        throw new ArgumentOutOfRangeException();

    var request = WebRequest.Create(url);
    while (true)
    {
        try
        {
            var result = await request.GetResponseAsync();
            return (HttpWebResponse)result;
        }
        catch (Exception ex)
        {
            if (--retries == 0)
                throw; // rethrow last error
            // otherwise, log the error and retry
            Debug.Print("Retrying after error: " + ex.Message);
        }
    }
}

More reading:

"Task.Factory.StartNew" vs "new Task(...).Start".

Task.Run vs Task.Factory.StartNew.



回答2:

I would recommend doing something like this:

private int retryCount = 3;
...

public async Task OperationWithBasicRetryAsync()
{
  int currentRetry = 0;

  for (; ;)
  {
    try
    {
      // Calling external service.
      await TransientOperationAsync();

      // Return or break.
      break;
    }
    catch (Exception ex)
    {
      Trace.TraceError("Operation Exception");

      currentRetry++;

      // Check if the exception thrown was a transient exception
      // based on the logic in the error detection strategy.
      // Determine whether to retry the operation, as well as how 
      // long to wait, based on the retry strategy.
      if (currentRetry > this.retryCount || !IsTransient(ex))
      {
        // If this is not a transient error 
        // or we should not retry re-throw the exception. 
        throw;
      }
    }

    // Wait to retry the operation.
    // Consider calculating an exponential delay here and 
    // using a strategy best suited for the operation and fault.
    Await.Task.Delay();
  }
}

// Async method that wraps a call to a remote service (details not shown).
private async Task TransientOperationAsync()
{
  ...
}

This code is from the Retry Pattern Design from Microsoft. You can check it out here: https://msdn.microsoft.com/en-us/library/dn589788.aspx