Ho appena fatto un'osservazione curiosa riguardo al Task.WhenAll
metodo, quando eseguivo .NET Core 3.0. Ho passato un'attività semplice Task.Delay
come argomento singolo a Task.WhenAll
, e mi aspettavo che l'attività avvolta si comportasse in modo identico all'attività originale. Ma non è così. Le continuazioni dell'attività originale vengono eseguite in modo asincrono (il che è desiderabile) e le continuazioni di più Task.WhenAll(task)
wrapper vengono eseguite in modo sincrono l'una dopo l'altra (il che è indesiderabile).
Ecco una demo di questo comportamento. Quattro attività di lavoro sono in attesa Task.Delay
di essere completate dalla stessa attività, quindi continuano con un calcolo pesante (simulato da a Thread.Sleep
).
var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");
await task;
//await Task.WhenAll(task);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
$" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");
Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);
Ecco l'output. Le quattro continuazioni vengono eseguite come previsto in thread diversi (in parallelo).
05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await
Ora, se commento la riga await task
e rimuovo il commento sulla riga seguente await Task.WhenAll(task)
, l'output è piuttosto diverso. Tutte le continuazioni sono in esecuzione nello stesso thread, quindi i calcoli non sono parallelizzati. Ogni calcolo inizia dopo il completamento del precedente:
05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await
Sorprendentemente ciò accade solo quando ciascun lavoratore attende un wrapper diverso. Se definisco il wrapper in anticipo:
var task = Task.WhenAll(Task.Delay(500));
... e quindi await
lo stesso compito in tutti i lavoratori, il comportamento è identico al primo caso (continuazioni asincrone).
La mia domanda è: perché sta succedendo questo? Che cosa fa sì che le continuazioni di diversi wrapper della stessa attività vengano eseguite nello stesso thread, in modo sincrono?
Nota: il wrapping di un'attività con Task.WhenAny
invece di Task.WhenAll
ottenere lo stesso strano comportamento.
Un'altra osservazione: mi aspettavo che l'avvolgimento del wrapper all'interno di a Task.Run
avrebbe reso le continuazioni asincrone. Ma non sta succedendo. Le continuazioni della riga seguente vengono comunque eseguite nello stesso thread (in modo sincrono).
await Task.Run(async () => await Task.WhenAll(task));
Chiarimento: le differenze di cui sopra sono state osservate in un'applicazione Console in esecuzione sulla piattaforma .NET Core 3.0. In .NET Framework 4.8, non vi è alcuna differenza tra l'attesa dell'attività originale o il wrapper di attività. In entrambi i casi, le continuazioni vengono eseguite in modo sincrono, nello stesso thread.
Task.WhenAll
Task.Delay
da 100
a in 1000
modo che non sia completato quando await
ed.
await Task.WhenAll(new[] { task });
?