L'illusione Del Parallelismo

Tra i tanti concetti (e pre-concetti) dell’informatica, mi ha sempre affascinato un paradigma in particolare: l’illusione. In informatica a differenza di molte altre scienze sei libero di barare, nascondere e utilizzare tutti i trucchetti che vuoi per far credere all’utente qualsiasi cosa. Alcuni pattern su interfaccie grafiche e relative ad esperienze utente sono così subdoli da essere stati fortemente criticati da molte persone1.

Uno dei “trucchi di magia” che ancora mi rendono affascinato rimane il “finto parallelismo”. Immaginiamo di avere un dispositivo ad una CPU con un solo core e di voler ascoltare la musica in background mentre scriviamo il nuovo articolo da pubblicare su un blog. Dalla teoria di base dell’informatica sappiamo che un solo core può eseguire al massimo una operazione alla volta e, salvo casi particolari, non è possibile avere un parallelismo tra istruzioni.

Una persona potrebbe pensare che pigiando un tasto si genera una serie di connessioni tra CPU e I/O che mette in pausa il flusso “musicale”: si sentirebbero in questo caso diverse pause tra le note della melodia musicale. Eppure così non è. Se provassimo ad ascoltare musica su una piattaforma online e intanto scrivere su un qualsiasi editor di testo, non notiamo alcuna differenza tra diversi momenti. Come avviene quindi questo processo? È possibile che la macchina abbia più di un core e nessuno ce l’abbia detto?

Si tratta in realtà dell’illusione del parallelismo, un principio che fa credere all’utente di poter eseguire più processi contemporaneamente, quando in realtà sono solo diversi processi che eseguiti in modo sequenziale e ravvicinato danno l’illusione di avere il parallelismo. Questo concetto è anche chiamato multi-tasking e sfrutta la lentezza degli utenti. Come esempio, poniamo il caso di avere due processi p_1 e p_2. p_1 e p_2 si alternano ogni 10 ms in memoria ed ognuno di loro produce un tono diverso. L’utente non riesce a distinguere la differenza dei due toni musicali in così poco tempo e la mente in realtà fonderebbe i due toni in uno unico. (Stessa cosa avviene per qualsiasi altro tipo di evento dei due processi.)

Come funziona il meccanismo? È molto semplice: senza scendere in troppi dettagli, il sistema operativo assegna ad ogni processo un certo tempo limite entro il quale possono occupare la CPU, eseguendo le istruzioni del programma secondo il ciclo Fetch-Decode-Execute. Quando il tempo scade, ecco che il sistema operativo (agendo come una sorta di arbitro) assegna lo stesso tempo limite ad un altro processo e così via. Durante il cambio da un processo ad un altro, avviene il cosidetto context switch, ovvero cambio di contesto. Infatti non è detto che p_1 o p_2 abbiano finito le loro operazioni nel momento in cui scade il tempo (nella maggior parte delle volte un processo passa varie volte da pronto ad esecuzione.)

L’illusione prodotta dal sistema operativo crea un parallelismo a cui l’utente crede quando vuole svolgere più operazioni diverse. È evidente che nel corso del tempo si è capito che un sistema ad un core poteva arrivare ad eseguire “contemporaneamente” un certo numero di processi, fino ad un numero limite. Da quel punto in poi si è cercato di adottare sistemi multi-core che potessero avere a disposizione più di una CPU.

Esempio di illusione

Tale concetto di illusione si illustra facilmente con questo esempio2:

#include <stdio.h>
#include <stdlib.h>

double GetTime() {
    struct timeval t;
    int rc = gettimeofday(&t, NULL);
    assert(rc == 0);
    return (double) t.tv_sec + (double) t.tv_usec/1e6;
}

void Spin(int quanto) {
    double time = GetTime();
    while ((GetTime() - time) < (double) quanto); 
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
	    fprintf(stderr, "utilizzo: ./cpu <string>\n");
	    exit(1);
    }

    // serve per stampare la stringa
    char *str = argv[1];

    while (1) {
	    printf("%s\n", str);
	    Spin(1);
    }
    return 0;
}

Questo codice non fa molto. Infatti, tutto il codice importante si concentra nel chiamare Spin(), una funzione che controlla ripetutamente il tempo e ritorna una volta che ha girato per un secondo. Poi, stampa la stringa che l’utente utente ha passato sulla linea di comando, e ripete, all’infinito.

Diciamo che salviamo questo file come cpu.c e decidiamo di compilarlo ed eseguirlo su un sistema con un singolo processore.

root@node: gcc cpu.c -o cpu
root@node: ./cpu "A"
A
A
A
A
A
A
^C

Una volta passato un secondo, il codice stampa la stringa di input passata dall’utente (in questo esempio, la lettera “A”), e continua. CTRL+C per fermare l’esecuzione. Ora facciamo un passo avanti, proviamo ad eseguire istanze multiple di questo programma utilizzando “&”.

root@node: ./cpu A & ./cpu B & ./cpu C & ./cpu D &
[1] 15353
[2] 15554
[3] 15230
[4] 19102
A
B
D
C
A
B
D
C
A
...

Bene, ora le cose si fanno un po’ più interessanti. Anche se abbiamo un solo processore, in qualche modo tutti e quattro questi programmi sembrano essere in esecuzione allo stesso tempo! Come avviene questa magia?

Si scopre che il sistema operativo, con qualche aiuto dall’hardware, è responsabile di questa illusione, cioè l’illusione che il sistema abbia un numero molto grande di CPU virtuali. Trasformare una singola CPU (o un piccolo serie di esse) in un numero apparentemente infinito di CPU e permettendo così l’esecuzione simultanea di molti programmi è ciò che si chiama virtualizzazione della CPU.


  1. Si chiamano dark patterns, ma questa è un’altra storia. ↩︎

  2. Esempio preso da “Operating Systems: Three Easy Pieces” di Remzi H. Arpaci-Dusseau. ↩︎