2016-05-30 32 views
6

Diciamo che ho un paio di compiti:Esiste un modo predefinito per ottenere la prima attività completata correttamente?

void Sample(IEnumerable<int> someInts) 
{ 
    var taskList = someInts.Select(x => DownloadSomeString(x)); 
} 

async Task<string> DownloadSomeString(int x) {...} 

voglio per ottenere il risultato della prima operazione di successo. Quindi, la soluzione di base è quella di scrivere qualcosa di simile:

var taskList = someInts.Select(x => DownloadSomeString(x)); 
string content = string.Empty; 
Task<string> firstOne = null; 
while (string.IsNullOrWhiteSpace(content)){ 
    try 
    { 
     firstOne = await Task.WhenAny(taskList); 
     if (firstOne.Status != TaskStatus.RanToCompletion) 
     { 
      taskList = taskList.Where(x => x != firstOne); 
      continue; 
     } 
     content = await firstOne; 
    } 
    catch(...){taskList = taskList.Where(x => x != firstOne);} 
} 

Ma questa soluzione sembra funzionare N + (N -1) + .. + K compiti. Dove N è someInts.Count e K è la posizione della prima attività riuscita nelle attività, in modo da eseguire di nuovo tutte le attività tranne una che viene acquisita da WhenAny. Quindi, esiste un modo per ottenere la prima attività completata correttamente con l'esecuzione di massimo N attività? (Se compito di successo sarà l'ultima)

+0

Mi sembra che tu voglia eseguire le attività in sequenza, non in parallelo, giusto? –

+0

Se vuoi eseguirli in parallelo, allora 'var taskList = someInts.Select (x => DownloadSomeString (x)). ToList()' dovrebbe funzionare per te. –

+0

Se vuoi eseguirli in sequenza, allora un semplice ciclo 'for' dovrebbe fare il lavoro. –

risposta

4

il problema con "il primo compito con successo" è cosa fare se tutte le attività falliscono? È un really bad idea to have a task that never completes.

Presumo che si desideri propagare l'eccezione dell'ultima attività se si verificano errori . Con questo in mente, direi che qualcosa di simile sarebbe opportuno:

async Task<Task<T>> FirstSuccessfulTask(IEnumerable<Task<T>> tasks) 
{ 
    Task<T>[] ordered = tasks.OrderByCompletion(); 
    for (int i = 0; i != ordered.Length; ++i) 
    { 
    var task = ordered[i]; 
    try 
    { 
     await task.ConfigureAwait(false); 
     return task; 
    } 
    catch 
    { 
     if (i == ordered.Length - 1) 
     return task; 
     continue; 
    } 
    } 
    return null; // Never reached 
} 

Questa soluzione si basa sulla OrderByCompletion extension method che part di my AsyncEx library; esistono anche implementazioni alternative per Jon Skeet e Stephen Toub.

+0

Non 'OrderByCompletion' aspetta che tutte le attività vengano completate prima? –

+1

Ho usato questo approccio generale nelle mie precedenti revisioni, se si guarda alla storia della mia risposta. Se ordini il compito è molto più semplice di quanto hai dimostrato di calcolare il valore, ma onestamente è più semplice di tutto questo aggiungere semplicemente le continuazioni a mano in questo caso. – Servy

+0

@YacoubMassad No. Questo è l'intero punto del metodo. È completamente asincrono e puoi immaginarlo semplicemente restituendo i compiti nell'ordine in cui finiranno (anche se tecnicamente non è ciò che sta accadendo sotto il cofano). – Servy

4

Tutto quello che devi fare è creare un TaskCompletionSource, aggiungere una continuazione a ciascuno dei vostri compiti, e impostarlo quando il primo concluso con successo:

public static Task<T> FirstSuccessfulTask<T>(IEnumerable<Task<T>> tasks) 
{ 
    var taskList = tasks.ToList(); 
    var tcs = new TaskCompletionSource<T>(); 
    int remainingTasks = taskList.Count; 
    foreach(var task in taskList) 
    { 
     task.ContinueWith(t => 
      if(task.Status == TaskStatus.RanToCompletion) 
       tcs.TrySetResult(t.Result)); 
      else 
       if(Interlocked.Decrement(ref remainingTasks) == 0) 
        tcs.SetException(new AggregateException(
         tasks.SelectMany(t => t.Exception.InnerExceptions)); 
    } 
    return tcs.Task; 
} 

e una versione per compiti senza un risultato:

public static Task FirstSuccessfulTask(IEnumerable<Task> tasks) 
{ 
    var taskList = tasks.ToList(); 
    var tcs = new TaskCompletionSource<bool>(); 
    int remainingTasks = taskList.Count; 
    foreach(var task in taskList) 
    { 
     task.ContinueWith(t => 
      if(task.Status == TaskStatus.RanToCompletion) 
       tcs.TrySetResult(true)); 
      else 
       if(Interlocked.Decrement(ref remainingTasks) == 0) 
        tcs.SetException(new AggregateException(
         tasks.SelectMany(t => t.Exception.InnerExceptions)); 
    } 
    return tcs.Task; 
} 
+0

Intendevi se (task.Status == TaskStatus.RanToCompletion)? – bodangly

+0

@bodangly l'ho fatto. – Servy

+0

Se si è reso restituire un 'Task >' è possibile semplificare la segnalazione di errore. È possibile restituire un compito non riuscito come risultato o null. Dipende in genere da ciò che il chiamante vuole raggiungere. – usr

2

Una soluzione semplice è attendere qualsiasi attività, controllare se è in stato RanToCompletion e, in caso contrario, attendere di nuovo qualsiasi attività tranne quella già terminata.

async Task<TResult> WaitForFirstCompleted<TResult>(IEnumerable<Task<TResult>> tasks) 
    { 
     var taskList = new List<Task<TResult>>(tasks); 
     Task<TResult> firstCompleted; 
     while (taskList.Count > 0) 
     { 
      firstCompleted = await Task.WhenAny(taskList); 
      if (firstCompleted.Status == TaskStatus.RanToCompletion) 
      { 
       return firstCompleted.Result; 
      } 
      taskList.Remove(firstCompleted); 
     } 
     throw new InvalidOperationException("No task completed successful"); 
    } 
+0

Questo è ciò che l'OP sta già facendo. – Servy

+0

@Servy Tutte le soluzioni fornite qui hanno fatto lo stesso (con un approccio diverso) - attendere N operazioni in esecuzione e restituire il primo task/risultato completato con successo. Questo (e anche gli altri) non * ha * rieseguito i compiti come menzionato nella domanda. –

0

versione modificata del codice @Servy s' perché contiene alcuni errori di compilazione e diverse insidie. La mia variante è:

public static class AsyncExtensions 
{ 
    public static Task<T> GetFirstSuccessfulTask<T>(this IReadOnlyCollection<Task<T>> tasks) 
    { 
     var tcs = new TaskCompletionSource<T>(); 
     int remainingTasks = tasks.Count; 
     foreach (var task in tasks) 
     { 
      task.ContinueWith(t => 
      { 
       if (task.Status == TaskStatus.RanToCompletion) 
        tcs.TrySetResult(t.Result); 
       else if (Interlocked.Decrement(ref remainingTasks) == 0) 
        tcs.SetException(new AggregateException(
         tasks.SelectMany(t2 => t2.Exception?.InnerExceptions ?? Enumerable.Empty<Exception>()))); 
      }); 
     } 
     return tcs.Task; 
    } 
} 

Non abbiamo al ToList il nostro contributo perché è già una collezione possiamo lavorare con, compila (vantaggio enorme) e gestisce la situazione quando eccezione per qualche motivo non ha un unica eccezione (è completamente possibile).

+0

Qual è il vantaggio di questo approccio (originariamente suggerito da @Servy) rispetto a quello di @Sir Rufo? Perché dovresti usare questo su quello? – 3615

+0

Prima di tutto, compila. In secondo luogo - non è necessario eseguire le attività di 'ToList' se sono già in collezione. Non è un grande sovraccarico, ma il codice originale ce l'ha. E penso che il contratto con il codice debba essere più trasparente. Se stiamo facendo 'ToList' sulla raccolta di sorgenti, allora stiamo mentendo sul fatto che possiamo gestire la query o qualcosa del genere, perché abbiamo effettivamente bisogno di una collezione. –