Visualizzazione post con etichetta c#. Mostra tutti i post
Visualizzazione post con etichetta c#. Mostra tutti i post

giovedì 7 marzo 2013

Programmazione asincrona in C# 5

C# versione 5 rende l'utilizzo della programmazione asincrona piuttosto agevole attraverso l'utilizzo di un pattern molto semplice e di due keyword: async e await.



Utilizzare codice "asincrono" è una necessità non solo quando si vuole parallelizzare l'esecuzione di procedure, ma anche quando si vuole rendere la logica di business indipendente dal codice che gestisce e muove l'interfaccia grafica. L'idea è che nessuna operazione debba "stoppare" l'esecuzione del codice deputato a gestire l'input dell'utente o a fornirgli un feedback.

Tanto più urgente è questa necessità quanto più il device ha processori poco potenti: che è il caso tipico dei device mobile. Ad esempio, il framework di programmazione di Windows Phone, WinRT, si basa pesantemente sul concetto di programmazione asincrona, ed il suo uso è obbligatorio. Non è possibile, giusto per citare un caso, in WinRTleggere un file in sincrono: è invece necessario predisporre il codice asincrono, in modo che mentre le operazioni di lettura del file sono in esecuzione, l'interfaccia e il resto del programma continuano a funzionare in parallelo.

La gestione della programmazione asincrona, benché semplice, è piuttosto controintuitiva. Su MSDN si legge che "l'operatore await dice al compilatore che il metodo marcato async non può continuare dopo quel punto fino a che il processo asincrono che si aspetta non è terminato". Cerchiamo di capire cosa significa con un esempio.

Supponiamo di avere una ipotetica funzione di business che ci impiega molto tempo per completare. Ad esempio questa, che su un portatile non troppo vecchio impiega qualche secondo:

static long LongRunningComputation(int i)
{
    Random cycleSeed = new Random();
    long result = 0; int rnd = 0;
    for (long j = 0; j < cycleSeed.Next() * 1E5 + 3E6; j++)
    {
        rnd = cycleSeed.Next(11);
        result += (rnd % 2 == 0) ? rnd : -rnd;
    }
    return result;
}

La prima cosa che occorre fare è incapsulare questa funzionalità in un metodo asincrono. Ogni metodo asincrono è tale se ritorna un oggetto Task. Un oggetto Task si costruisce con una Factory che si chiama Task.Run e prende in input una funzione lambda qualsiasi, con questa sintassi:

static async Task LongTaskAsync(int i)
{
    Task longOperation = 
            Task.Run(() > LongRunningComputation(i));

    return await longOperation;
}

La prima istruzione serve per costruire l'oggetto Task. Questo è un oggetto che mantiene lo stato e le informazioni sul processo asincrono che è in corso. Il fatto che la nostra operazione ritorni un intero viene riflesso nel fatto che Task è di fatto un contenitore generico (di un intero lungo).

La funzione lambda è in grado di "leggere" i parametri che sono in scope nel momento in cui è definita, quindi non c'è bisogno di specificare che intendiamo passargli l'intero "i" - questa cosa io ci ho messo francamente un po' per capirla... io avrei messo

((i) => LongRunningComputation(i))

 però il parametro di Task.Run è un delegato che non accetta parametri!

La norma vuole che ogni funzione "asincrona" abbia un nome che termina in "Async" e non ci adeguiamo. La parola "await" significa che questo metodo deve aspettare il termine dell'operazione "LongRunningComputation" prima di tornare un valore che abbia senso! Infatti il codice LongRunningComputation viene lanciato in asincrono, e quindi la linea di codice successiva viene eseguita immediatamente.

Senza la programmazione asincrona, si potrebbe pensare di costruire una funzione che esegue due chiamate all'operazione lunga e somma i loro risultati, così:

static long GoInLineNormally()
{
    Console.WriteLine("Going in sequence, normally");
    long x1 = LongRunningComputation(11);
    Console.WriteLine("X1 (NORMAL) = " + x1);
    long x2 = LongRunningComputation(22);
    Console.WriteLine("X2 (NORMAL) = " + x2);
    return (x1 + x2);
}

Siccome abbiamo detto che l'operazione lunga ci mette grossomodo due secondi, la procedura di cui sopra ci metterebbe circa quattro secondi per essere eseguita.

Ma abbiamo la programmazione asincrona, e quindi è lecito pensare che le due possano essere lanciate sue due thread differenti, e quando ciascun thread esce con un risultato, sommarli e stamparli a video.

static async Task GoInParallelAsync()
{
    Console.WriteLine("Going in parallel");

    var t1 = LongTaskAsync(11);
    var t2 = LongTaskAsync(22);

    await Task.WhenAll(t1, t2);

    Console.WriteLine("X1 (PARALLEL) = " + t1.Result);
    Console.WriteLine("X2 (PARALLEL) = " + t2.Result);
    return (t1.Result + t2.Result);
}


Per farlo abbiamo lanciato LongTaskAsync e poi, attraverso la funzione Task.WhenAll, abbiamo "aspettato" che entrambi finissero per raccogliere i risultati. L'esecuzione totale, come ci aspettavamo è di circa due secondi invece che quattro.

Ricapitolando. Se un metodo asincrono viene lanciato con l'operatore "await", l'esecuzione si ferma finché il metodo non ha ritornato un risultato - ma questo metodo viene comunque lanciato su un thread differente rispetto al programma che lo ha lanciato, il quale può ad esempio continuare così ad aggiornare l'interfaccia grafica e a rimanere in ascolto di eventuali eventi. Se invece un metodo asincrono viene lanciato senza l'operatore await, la linea successiva di codice viene eseguita immediatamente, e il controllo sull'esecuzione del task asincrono è demandato ai metodi della classe Task.

Infatti, se lanciamo con:

static async Task GoInLineAsync()
{
    Console.WriteLine("Going in sequence, but async");
    long x1 = await LongTaskAsync(11);
    Console.WriteLine("X1 (SEQUENCE) = " + x1);
    long x2 = await LongTaskAsync(22);
    Console.WriteLine("X2 (SEQUENCE) = " + x2);
    return (x1 + x2);
}


il codice esegue prima LongTaskAsync(11) e solamente dopo aver ottenuto il risultato (ciò che è espresso dall'operatore await), lancia il LongTaskAsync(22).

Questo codice, benché funzionalmente identico a GoInLineNormally è molto diverso dal punto di vista del comportamento! Il primo infatti è rigorosamente sincrono: ogni linea di codice viene eseguita solo e soltanto se quella precendente è terminata. Il secondo, invece, attraverso l'operatore "await" dice al compilatore: adesso esegui in asincrono su un thread differente LongTaskAsync, e aspetta che il metodo asincrono sia terminato prima di proseguire con la prossima linea di codice.

E' da notare in particolare il codice entry point, Main, che richiama le procedure scritte sopra:

static void Main(string[] args)
{
    Console.WriteLine("AYNC/AWAIT TEST v.1.0");
    Console.WriteLine();

    //long r = GoInLineNormally();
    var t = GoInLineAsync();
    //var t = GoInParallelAsync();

    Console.WriteLine("END TEST (Is it really ended?)");
    Console.WriteLine("Result = " + t.Result);
    Console.WriteLine();
    Console.ReadKey();

}

Domanda: quando viene stampata la linea "END TEST"?

Alla fine dell'esecuzione dei vari metodi GoIn...? La risposta è sì, se si tratta di GoInLineNormally(), ma è no in entrambi gli altri casi: si tratta di metodi asincroni che ritornano subito (non c'è await)! Quindi "END TEST" non è proprio un messaggio di End, perché viene stampato immediatamente, mentre i metodi asincroni sono in esecuzione. Viceversa, "Result =" viene stampato effettivamente alla fine dei calcoli, perché il valore di t è "awaited" da t.Result.

E se avessimo voluto "aspettare" l'esecuzione GoInLineAsync() prima di scrivere "END"? Non avremmo potuto farlo! Infatti un metodo che contiene una chiamata "await" deve per forza essere marcato "async". Ma il metodo Main non può essere "async"! Quindi non abbiamo potuto scrivere, per esempio:

var t = await GoInLineAsync()

Se avessimo marcato il metodo Main come "async", avremmo detto al compilatore che quel metodo va eseguito in asincrono e che deve ritornare immediatamente al chiamante, per poi "rientrare" quando il metodo sia terminato: due cose che un programma a console, evidentemente, non può fare.


venerdì 28 dicembre 2012

Sviluppare codice migliore con la programmazione funzionale


Minimizzare gli impatti del cambiamento dovrebbe essere uno degli obiettivi prioritari nella produzione del software, soprattutto se in contesti di medio-alta complessità. Normalmente, infatti, si assiste al fenomeno dell'ingessatura del codice: nessuno si azzarda a introdurre cambiamenti migliorativi perché questo potrebbe portare a conseguenze inaspettate, potenzialmente drammatiche.

Per minimizzare gli impatti del cambiamento esistono strumenti e tecniche diverse: ad esempio il test driven development, oppure il defensive programming. Un altro metodo è quello di scrivere codice secondo il paradigma della programmazione funzionale (FP = Functional Programming).




Un programma scritto in un linguaggio FP puro è normalmente esente da "side-effect": cioè è un software che si comporta in un solo modo, predicibile, e che ha un comportamento identico una volta compilato. Questo perché si traduce sostanzialmente in una funzione che prende in input una qualunque serie di funzioni che agiscono su costanti. E' come un meccanismo di ruote e pulegge: una volta in movimento, si comporta sempre nello stesso identico modo.

Purtroppo solo una piccolissima parte di programmi può essere scritta in FP pura. Questo perché un programma per essere utile deve poter prevedere: 1) input dall'esterno e 2) mantenimento di uno stato. Queste però sono due cose che sono "inesprimibili" in FP puro.

Quello che possiamo fare è invece utilizzare un linguaggio ibrido, che permetta cioè sia la programmazione imperativa (magari ad oggetti) cui siamo abituati, sia la programmazione funzionale, cercando di minimizzare e confinare la parte imperativa in un punto noto - e ridotto - del codice sorgente. Eccellenti esempi di linguaggi ibridi sono oggi Scala e C#.

In Scala, le variabili vengono nominate in due modi diversi: var, la variabile "normale" cui siamo abituati, e val, una variabile che, una volta che abbia assegnato un valore, non lo modificherà mai per tutta la sua vita. (In C# le variabili val di Scala sono scritte come readonly) Un programma scritto in Scala che abbia molte var, è un programma che segue la logica imperativa, che crea oggetti che mantengono uno stato e che quindi è potenzialmente pieno di side-effects. Viceversa, un programma che abbia pochissime o nessuna var, e val al loro posto, è un programma scritto in logica funzionale, e che avrà pochi o nessun side-effect. Questo secondo tipo di programma è molto più facile da mantenere, dovrà essere testato di meno, e quindi avrà maggiore valore.

Come si possono scrivere normali costrutti in logica funzionale? E' difficile, perché il nostro cervello è abituato a pensare in termini di "oggetti" e di "stato", e molto poco in termini di "funzioni" e "funzioni di funzioni". Però con un po' di pratica l'obiettivo può essere centrato.

Vediamo un esempio. Supponiamo di voler scrivere una classe che prende in input una stringa e ne calcola il suo valore ASCII (il valore ASCII di una stringa è la somma dei suoi caratteri intesi secondo la codifica ASCII).


class TestClass(givenString: String) {

    require(givenString != null)
    private var gStr: String = givenString


    def getValue() = {
        this.compute()
    }

    private def compute(): Int = {
        var tempSum: Int = 0
        for (charV <this.gStr.toCharArray()) {
            tempSum += charV.toInt
        }
        tempSum
    }

}


Questo programma in Scala contiene due var: una per mantenere lo "stato", che è la variabile che contiene la stringa da trasformare in numero, e una per mantenere il numero associato alla stringa mentre viene calcolato, all'interno di un ciclo for.

Come trasformare questa classe in logica funzionale?

Dobbiamo come prima cosa far sparire lo "stato" dalla classe. La nostra classe deve comportarsi come una "funzione" e quindi non deve mantenere in memoria parametri che possono cambiare valore nel tempo. Farlo è molto semplice: invece che memorizzare givenString come variabile (var) lo memorizzeremo come costante (val). Oppure, potremmo non memorizzarlo del tutto e passarlo così com'è al metodo di calcolo compute().

Più difficile è invece sostituire il costrutto for che calcola il valore. Qui useremo invece una formula classica della FP. Creeremo un metodo che ritorna una collezione, cioè un array dinamico, di valori ASCII, ognuno per ciascun carattere che forma la stringa. Poi su questa collezione applicheremo una funzione di trasformazione: nel nostro caso "sum". Ogni collezione fornisce metodi operatore sui valori che la compongono, e in più può accettarne di nuovi appositamente scritti.

Ecco il nostro codice trasformato in logica funzionale:

class ImmutableTestClass(givenString: String) {

    require(givenString != null)
    private val gStr: String = givenString

    def getValue() = {
        this.compute(this.gStr)
    }

    private def listOfValues(valStr: String) = {
        for (j <- 0 until valStr.length()) yield {
            valStr.charAt(j).toInt
        }
    }

    // 1. Convert the sequence to actual collection
    // 2. Add "mapping" to collection, example: .sum, .mkString
    private def compute(valStr: String): Int = {
        val listOfVals: List[Int] = this.listOfValues(valStr).toList
        listOfVals.sum
    }

}


Abbiamo buttato via le var e le abbiamo sostituite con val. Il codice è ora puramente funzionale. (E, tra parentesi, funzionerà molto meglio in contesti multi-threading).

Naturalmente lo stesso risultato si può ottenere con C#:

class Immutable
{
    private readonly String tString;

    public Immutable(String testString)
    {
        this.tString = testString;
    }

    public int GetSum()
    {
        return this.CharList().Sum();
    }

    private IEnumerable<int> CharList()
    {
        foreach (char ch in this.tString)
        {
            yield return (int)ch;
        }
    }
}