Elaborare le attività asincrone non appena vengono completate (C#)

Usando Task.WhenAny, è possibile avviare più attività contemporaneamente ed elaborarle una ad una quando vengono completate, invece che nell'ordine in cui vengono avviate.

Nell'esempio seguente viene usata una query per creare una Collection di attività. Ogni attività scarica il contenuto di un sito Web specificato. In ogni iterazione di un ciclo while, una chiamata attesa a WhenAny restituisce l'attività nella Collection di attività che completa per prima il download. Questa attività viene rimossa dalla Collection ed elaborata. Il ciclo si ripete finché la Collection non contiene più attività.

Prerequisiti

È possibile seguire questa esercitazione usando una delle opzioni seguenti:

  • Visual Studio 2022 con il carico di lavoro Sviluppo per desktop .NET installato. .NET SDK viene installato automaticamente quando si seleziona questo carico di lavoro.
  • .NET SDK con un editor di codice di propria scelta, ad esempio Visual Studio Code.

Creare un'applicazione di esempio

Creare una nuova applicazione console .NET Core. È possibile crearne una usando il comando dotnet new console o da Visual Studio.

Aprire il file Program.cs nell'editor di codice e sostituire il codice esistente con questo codice:

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Aggiungi campi

Nella definizione della classe Program aggiungere i due campi seguenti:

static readonly HttpClient s_client = new HttpClient
{
    MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/xamarin"
};

HttpClient espone la possibilità di inviare richieste HTTP e ricevere risposte HTTP. s_urlList contiene tutti gli URL che l'applicazione prevede di elaborare.

Aggiornare il punto di ingresso dell'applicazione

Il punto di ingresso principale nell'applicazione console è il metodo Main. Sostituire il metodo esistente con quanto segue:

static Task Main() => SumPageSizesAsync();

Il metodo Main aggiornato viene ora considerato un Async Main, che consente un punto di ingresso asincrono nel file eseguibile. Viene espresso come chiamata a SumPageSizesAsync.

Creare il metodo SumPageSizes asincrono

Sotto il metodo Main aggiungere il metodo SumPageSizesAsync:

static async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
    Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
}

Il ciclo while rimuove una delle attività in ogni iterazione. Al termine di ogni attività, il ciclo termina. Il metodo inizia creando un'istanza e avviando una classe Stopwatch. Include quindi una query che, quando eseguita, crea una raccolta di attività. Ogni chiamata a ProcessUrlAsync nel codice seguente restituisce un Task<TResult>, dove TResult è un valore intero:

IEnumerable<Task<int>> downloadTasksQuery =
    from url in s_urlList
    select ProcessUrlAsync(url, s_client);

A causa dell'esecuzione posticipata con LINQ, si chiama Enumerable.ToList per avviare ogni attività.

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

Il ciclo while esegue i passaggi seguenti per ogni attività nella raccolta:

  1. Attende una chiamata a WhenAny per identificare la prima attività nella raccolta che ha terminato il download.

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. Rimuove l'attività dalla Collection.

    downloadTasks.Remove(finishedTask);
    
  3. Attende finishedTask, che viene restituito da una chiamata a ProcessUrlAsync. La variabile finishedTask è un Task<TResult> dove TResult è un valore intero. L'attività è già stata completata, ma è possibile metterla in attesa per recuperare la lunghezza del sito Web scaricato, come illustrato di seguito. Se l'attività è in errore, await genererà la prima eccezione figlio archiviata in AggregateException, a differenza della lettura della proprietà Task<TResult>.Result, che genererebbe AggregateException.

    total += await finishedTask;
    

Aggiungere un metodo di elaborazione

Aggiungere il metodo ProcessUrlAsync seguente sotto il metodo SumPageSizesAsync:

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

Per qualsiasi URL specificato, il metodo userà l'istanza di client fornita per ottenere la risposta come byte[]. La lunghezza viene restituita dopo l'URL e la lunghezza viene scritta nella console.

Eseguire il programma più volte per verificare che le lunghezze scaricate non siano sempre nello stesso ordine.

Attenzione

È possibile usare WhenAny in un ciclo, come descritto nell'esempio, per risolvere i problemi che includono un numero limitato di attività. Tuttavia, se ci sono molte attività da elaborare, altri approcci sono più efficienti. Per altre informazioni ed esempi, vedere Processing tasks as they complete (Elaborazione di attività completate).

Esempio completo

Il codice seguente è il testo completo del file Program.cs per l'esempio.

using System.Diagnostics;

HttpClient s_client = new()
{
    MaxResponseContentBufferSize = 1_000_000
};

IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/xamarin"
};

await SumPageSizesAsync();

async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:    {total:#,#}");
    Console.WriteLine($"Elapsed time:              {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

// Example output:
// https://learn.microsoft.com                                      132,517
// https://learn.microsoft.com/powershell                            57,375
// https://learn.microsoft.com/gaming                                33,549
// https://learn.microsoft.com/aspnet/core                           88,714
// https://learn.microsoft.com/surface                               39,840
// https://learn.microsoft.com/enterprise-mobility-security          30,903
// https://learn.microsoft.com/microsoft-365                         67,867
// https://learn.microsoft.com/windows                               26,816
// https://learn.microsoft.com/xamarin                               57,958
// https://learn.microsoft.com/dotnet                                78,706
// https://learn.microsoft.com/graph                                 48,277
// https://learn.microsoft.com/dynamics365                           49,042
// https://learn.microsoft.com/office                                67,867
// https://learn.microsoft.com/system-center                         42,887
// https://learn.microsoft.com/education                             38,636
// https://learn.microsoft.com/azure                                421,663
// https://learn.microsoft.com/visualstudio                          30,925
// https://learn.microsoft.com/sql                                   54,608
// https://learn.microsoft.com/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

Vedi anche