Task.WhenAll - Come ottenere risultati da await multipli

La scorsa settimana, dopo avere scritto la prima versione del pacchetto Nuget SwitchyLinkGenerator ho guardato i tempi ed ho deciso di ottimizzare la parte critica.

  • Situazione: La chiamata verso la loro API impiega circa 2 secondi per tornare il link
  • Partenza: Avevo da fare circa 6 chiamate. In maniera sequenziale voleva dire buttare via circa 12 secondi
  • Modifica: Ottimizzazione del codice sfruttando Task.WhenAll

In questo articolo non verra’ spiegato nulla per quanto riguarda la TAP (Task Asynchronous Programming). Se avete dubbi in merito vi rimando all’articolo scritto lo scorso anno “HttpClient GetAsync - Asynchronous Coding & Performance –> Effettua chiamate HttpClient in Asynchronous (async/await) per ottimizzare il tuo codice

Prerequisiti

Ora -senza perdere molto altro tempo in parole- vediamo di passare al codice. Meglio, vero?

Facciamo mente locale, cosa mi serve?

  • Un DTO per i risultati
  • Un “raccoglitore” di Task
  • N chiamate async da fare
  • Un “raccoglitore” di Result

Ora che abbiamo tutto quanto possiamo cominciare. Pronti?

DTO

1
2
3
4
public class ResultDTO
{
  public string Id { get; set; }
}

List - Task

1
List<Task<ResultDTO>> tasks = new List<Task<ResultDTO>>();

foreach / async

1
2
3
4
5
foreach (var item in items)
{
    var t = _helper.CreateTaskAsync(...);
    tasks.Add(t);
}

List - Result

1
List<ResultDTO> result = new List<ResultDTO>();

Task.WhenAll & Result

Dopo avere preparato il campo coi precedenti prerequisiti dobbiamo affrontare la parte fondamentale di questo tutorial.

come aspetto i task per ottenere i risultati?

Nulla di piu’ semplice! Pronto?

Task WhenAll

Dopo avere scritto il foreach / async abbiamo la necessita’ di inserire -finalmente- il tanto atteso Task WhenAll

1
await Task.WhenAll(tasks.AsEnumerable());

await result

1
2
3
4
5
foreach (var task in tasks)
{
    var item = await task;
    result.Add(item);
}

Source Code

Dopo avere visto il codice sorgente spezzettato per capirlo al meglio, eccolo scritto tutto assieme direttamente dalla mia classe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public async Task<List<ResultDTO>> GenerateLink(DTO dto)
{
    List<Task<ResultDTO>> tasks = new List<Task<ResultDTO>>();

    foreach (var item in dto.Items)
    {
        var t = _helper.CreateTaskAsync(item);
        tasks.Add(t);
    }

    await Task.WhenAll(tasks.AsEnumerable());

    List<ResultDTO> result = new List<ResultDTO>();

    foreach (var task in tasks)
    {
        var item = await task;
        result.Add(item);
    }

    return result
        .Where(x => x != null && string.IsNullOrEmpty(x.Id) == false)
        .OrderBy(x => x.Id)
        .ToList();
}

Task WhenAll

Prima di chiudere, ecco per voi la definizione di Task.WhenAll direttamente dalla classe Task presente nel namespace System.Threading.Tasks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// Summary:
//     Creates a task that will complete when all of the System.Threading.Tasks.Task`1
//     objects in an enumerable collection have completed.
//
// Parameters:
//   tasks:
//     The tasks to wait on for completion.
//
// Type parameters:
//   TResult:
//     The type of the completed task.
//
// Returns:
//     A task that represents the completion of all of the supplied tasks.
//
// Exceptions:
//   T:System.ArgumentNullException:
//     The tasks argument was null.
//
//   T:System.ArgumentException:
//     The tasks collection contained a null task.
public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);