Articoli di programmazione

Async e Await: un errore molto comune

Molti sviluppatori credono di scrivere codice asincrono semplicemente aggiungendo queste 2 paroline magiche. La realtà è molto diversa.

photo-1579373903781-fd5c0c30c4cd.jpeg
Per prima cosa definiamo cosa fa del codice asincrono con un esempio semplicissimo. Tu stai preparando la colazione, versi il latte nella tazza e la metti nel microonde per scaldarlo. Mentre la tazza di latte si scalda, accendi la macchina del caffè e te ne fai una tazzina. Quando il microonde si spegne metti il latte in tavola insieme al caffè e puoi inzia re a fare la tua colazione.
Il passaggio importante è: mentre la tazza di latte di scalda nel microonde, fai il caffè. La tazza nel microonde che si scalda, siccome ho scritto "mentre", rappresenta una operazione asincrona.

Nel linguaggio di programmazione c# ha introdotto async per contraddistinguere un metodo asincrono. Await per attendere il risultato di una elaborazione asincrona ed intanto, nell'attesa, ritornare il controllo al chiamante.
Ogni metodo void diventa un metodo che ritorna un Task.
Ogni metodo che ritorna un tipo T, diventa un metodo che ritorna Task di T.
Ogni metodo asincrono, deve terminare per Async (è una convenzione).

Molti, sbagliando, scriverebbero il corpo del proprio metodo così:

var latte = await ScaldaLatteAsync();
var caffè = await PreparaCaffèAsync();

ServiColazione(latte, caffè);

Invece il codice corretto, per sfruttare l'elaborazione asincrona è il seguente:

Task<Latte> latteTask = ScaldaLatteAsync();
Task<Caffè> caffèTask = PreparaCaffèAsync();

var latte = await latteTask;
var caffè = await caffè Task;

ServiColazione(latte, caffè);

Perché il secondo blocco di codice è davvero asincrono mentre il primo blocco è un "finto asincrono"?
Perché await mette in attesa il metodo corrente e fa ritornare il controllo al metodo chiamante, finché l'operazione non è terminata ed il risultato è disponibile. Per questo await va chiamato solo quando abbiamo bisogno del risultato e non prima. Chiamare await successivamente ci dà l'opportunità di eseguire altre operazioni in parallelo, nel nostro caso preparare il caffè.
Per essere ancora più chiari:
- nel primo blocco si scalda il latte e DOPO si prepara il caffè
- nel secondo blocco si scalda il latte MENTRE si prepara il caffè

Un possibile affinamento, utile soprattutto quando i metodi asincroni non hanno risultato è il seguente, cioè utilizzare Task.WhenAll:

await Task.WhenAll(latteTask, caffèTask);
Console.Log("La colazione è pronta");

Un altro possibile affinamento consiste nell'uso di Task.WhenAny che accetta in ingresso una List di Tasks e restituisce il primo Task che termina la propria operazione.


var breakfastTasks = new List<Task> { latteTask, caffèTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == latteTask)
{
Console.WriteLine("latte is ready");
}
else if (finishedTask == caffèTask)
{
Console.WriteLine("caffè is ready");
}
breakfastTasks.Remove(finishedTask);
}


Per completezza riporto un semplice test di esempio, con la differenza di prestazioni tra la chiamata sincrona e quella asincrona. Sul mio Notebook la sincrona gira in 12 secondi, mentre la asincrona in 7:

[TestClass]
public class AsyncTest
{
[TestMethod]
public void AsyncAwait1()
{
var a = new AsyncExample();
var s = new Stopwatch();
s.Start();
Assert.AreEqual("OneTwo", a.ExecuteSync());
Debug.WriteLine("Sync executed in: " + s.ElapsedMilliseconds);
s.Reset();

s.Start();
var t = Task.Run( () => a.ExecuteAsync());
Assert.AreEqual("OneTwo", t.Result);
Debug.WriteLine("Async executed in: " + s.ElapsedMilliseconds);
s.Reset();

}
}

internal class AsyncExample
{
public AsyncExample()
{
}

internal string ExecuteSync()
{
return Operation1() + Operation2();
}

private string Operation2()
{
System.Threading.Thread.Sleep(5);
return "Two";
}

private string Operation1()
{
System.Threading.Thread.Sleep(5);
return "One";
}

internal async Task<string> ExecuteAsync()
{
var o1 = Operation1Async();
var o2 = Operation2Async();

var o1Result = await o1;
var o2Result = await o2;
return o1Result + o2Result;
}

private async Task<string> Operation2Async()
{
return await Task.Run(() => {
System.Threading.Thread.Sleep(3);
return "Two";
});
}

private async Task<string> Operation1Async()
{
return await Task.Run(() => {
System.Threading.Thread.Sleep(3);
return "One";
});
}
}


Un'ultima cosa.
Mi è capitato spesso di parlare di Async e Await con altri sviluppatori ed erano galvanizzati dall'idea di rendere asincrone le loro applicazioni web. Beh, dall'idea che avevano loro, sembrava che rendere Async un Controller permettesse al browser di elaborare in modo asincrono le richieste.
NIENTE DI PIU' FALSO
La richiesta Http e la risposta Http non cambiano! L'architettura HTTP bastata su HttpRequest e HttpResponse, non cambia: è sempre sincrona. Semplicemente puoi svolgere più operazioni in parallelo sul server mente viene elaborata la HttpResponse e quindi eventualmente l'applicazione è più veloce a rispondere.
Se vuoi rendere la tua pagina web asincrona, che ad esempio mentre carica una griglia, carica anche un menù e in parallelo l'utente può cliccare gli elementi che vede come i bottoni e vedere cambiare la UI, devi farlo lato client e puoi farlo in diversi modi, come ad esempio scrivere un client React , Vue.Js o Blazor, che faccia uso di funzioni asincrone per caricare i suoi dati, ma questa è un'altra storia (che vedremo).

#csharp #programmazione