.. meta:: :language: it :description language=it: processi e thread nei sistemi distribuiti :description language=en: processes and threads in distributed systems :keywords: distributed system, processes, threads :author: Luciano De Falco Alfano .. index:: processi e threads .. _ref_processi_e_threads: Processi e Thread =================== .. contents:: :local: *Lezione del 2019-04-17 mer*. Processi e thread sono alla base della costruzione dei servers, ma possono essere utilizzati anche nella implementazione di client. .. index:: processi .. _ref_processi: Processi ---------- Un processo è un programma in esecuzione. Similmente alle persone: * vengono generati; * hanno una vita; * opzionalmente possono generare uno o più figli; * eventualmente muoiono. A differenza delle persone (per ora): * hanno un solo genitore. L'esecuzione dei processi può avvenire secondo modelli diversi: * in sequenza, ovvero uno dopo l'altro, utilizzando un singolo processore; * in multiprogrammazione, con tanti processori quanti sono i processi; * in interleaving (multitasking), ovvero con un solo processore attivo che esegue una parte del processo, per poi passare al successivo, e così via a rotazione. Un processo può essere in uno dei seguenti stati: * attivo (running); * fermo (blocked) in attesa di una risorsa; * pronto (ready) a ripartire. Le transizioni tra i diversi stati sono illustratae dal seguente diagramma: .. image:: ./images/09_stati_di_un_processo.svg :align: center La creazione di processi è dispendiosa, richiede un rilevante overhead in quanto è necessario: * avere uno spazio degli indirizzi indipendente; * inizializzare la memoria; * azzerare il segmento dati; * copiare il programma nel segmento relativo; * ... Anche il passaggio da un processo all'altro è impegnativo. È necessario: * salvare il contesto della CPU (registri, program counter, open files, stack pointer, ...); * scambiare i processi tra memoria e disco; * caricare il nuovo processo; * ... .. index:: thread .. _ref_thread: Thread ---------- Per evitare l'overhead dovuto alla gestione dei processi, si può ricorrere al concetto di thread. I thread sono più flussi di istruzioni, che condividono lo stesso processo, e quindi la stessa memoria. D'altro canto, per poter essere flussi di istuzioni diversi, ognuno di essi dovrà avere: * la propria immagine della CPU, * il proprio program counter, * il proprio stack, * un proprio stato. Mentre saranno in comune, perché di competenza del processo: * lo spazio degli indirizzi, * le variabili globali, * i file aperti, * i processi figli, * gli allarmi da processare, * i segnali e i relativi gestori, * le informazioni di account. A differenza dei processi, che sono gestiti (creati, ...) tramite chiamata a sistema operativo, i thread sono gestiti in gran parte a livello applicativo. Questo evita l'overhead per l'escalation dei privilegi da livello user a livello kernel. Per questi motivi lo switch tra thread è molto più veloce. Così come la comunicazione tra thread tramite memoria condivisa. Nel caso di comunicazioni tra processi è necessario usare funzioni IPC a livello kernel, molto più lente della comunicazione tramite memoria condivisa. Però i thread hanno alcuni svantaggi: * essendo nello stesso processo, non sono protetti l'uno contro l'altro; * lo sviluppo di applicazioni multithread richiede un notevole sforzo intellettuale addizionale. Tutto ciò ha impatto sopratutto a livello di server. Perchè un server monoprocesso, quando riceve una singola richiesta da un client non ne può accettarne altre finché non l'ha soddisfatta e si è svincolato. Di conseguenza i server hanno sempre una struttura multiprocesso o multithread. In pratica avrò un processo che accoglie una richiesta (dispatcher) e la passa ad un altro processo e/o thread esecutore (worker). Quindi senza attendere il completamento dell'esecuzione, il dispatcher si rimette in ascolto per ricevere un'altra eventuale richiesta. La risposta al richiedente la invierà il worker. Se lo scopo dello sviluppo è ottenere un servizio particolarmente performante, allora conviene affrontare le difficoltà imposte dallo sviluppo a thread. Invece se il servizio sarà particolarmente critico in termini di sicurezza, conviene affidarsi alla tecnologia dei processi, che richiede più risorse, è più lenta, ma lo sviluppo sarà molto più affidabile. Lo sforzo necessario per sviluppare a thread è in gran parte dovuto alla necessità di gestire la memoria condivisa. Il fatto che questa sia in comune tra i thread del processo impone una strategia di accesso: non si può programmare lasciando libero ogni thread di accedere alla stessa area di memoria senza nessun vincolo: i dati diverranno rapidamente del tutto inaffidabili. D'altro canto non è neanche possibile bloccare (tramite lock) una risorsa dall'inizio alla fine di una operazione effettuata da thread: si potrebbero avere problemi di deadlock. E, ammesso non vi siano deadlock, comunque si rischia di vanificare l'incremento di prestazioni cercato con il multithread, perché si ricadrebbe in un modello di esecuzione seriale dei thread [#]_. I thread sono estremamente importanti per i sistemi distribuiti, perché permettono: * l'accesso a chiamate a sistema bloccanti senza fermare l'intero processo; * avere connessioni multiple contemporanemente. Sono utili sia in ambito server, che in ambito client. In ambito client l'uso di thread si può usare per tentare di mascherare i ritardi di comunicazione. Ad esempio i web browser li usano per caricare in parallelo elementi della pagina da visualizzare, in particolare le immagini, o gli elementi multimediali, mentre un altro thread si occupa della visualizzazione della struttura di base della pagina e del relativo testo. Tutto ciò ha successo se le diverse componenti possono essere realmente scaricate in parallelo, ovvero, se il server ha delle repliche. D'altro canto i server web con elevati carichi hanno praticamente sempre soluzioni di replica o di load balancing. In questo scenario l'uso di multithread da parte dl client diminuisce i tempi di risposta, anche se a scapito di un notevole uso delle risorse: CPU, RAM, ... Altro motivo dell'utilizzo del multithread lato client è la volontà di evitare di congelare l'interfaccia utente mentre si stanno effettuando operazioni che possono essere bloccanti, quali l'attesa di dati dalla rete. Considerazioni analoghe si applicano lato server. Ad esempio un server connesso via TCP/IP può riscontrare blocchi temporanei di comunicazione a causa di problemi di buffering se non si scoda con elevata velocità le richieste entranti. Da qui l'architettura dispatcher/workers. In cui i workers possono essere addirittura già pronti. Se i thread non sono disponibili, allora sul server è necessario utilizzare chiamate asincrone, utilizzando una finite state machine che tenga conto dello stato dell'elaborazione, e dando comunque priorità allo scodamento delle richieste. Nei sistemi embedded le chiamate sono sempre asincrone perché il sistema operativo è estremamente scarno, non vi è threading. Quindi è necessario utilizzare chiamate asincrone con callback. Questa possibiltà di programmazione è complessa. Per questo motivo non è possibile utilizzarla per lo sviluppo di grossi sistemi. .. index:: virtualizzazione .. _ref_virtualizzazione: Virtualizzazione -------------------- Ogni sistema distribuito di computer offre una interfaccia di chiamata verso applicazioni software di livello superiore. La **virtualizzazione** modifica l'interfaccia esistente per mimare il comportamento di un altro sistema. Il seguente diagramma illustra il concetto. .. image:: ./images/10_virtualizzazione.svg :align: center Questo concetto è stato introdotto negli anni \ ``'``\ 70 per permettere di continuare a fare girare il software precedente su nuove generazioni di mainframe. Molto usato da IBM nella famiglia dei 370. Nel contesto odierno si assiste ad una veloce evoluzione di hardware e software di base, mentre il livello applicativo è più stabile. La virtualizzazione aiuta a rendere disponibile le precedenti interfacce nelle nuove piattaforme. Inoltre gli amministratori di sistema possono gestire un solo tipo di piattaforma. Le applicazioni girano ognuna nella propria macchina virtuale, che a sua volta gira nella piattaforma standard [#]_. Vi sono i seguenti modi di implementare la virtualizzazione. Si può utilizzare un **runtime system** che fornisce un insieme astratto di istruzioni utilizzabili dalle applicazioni. Questo *runtime system* può essere un interprete o l'emulazione di un sistema operativo [#]_. Il seguente diagramma illustra questo schema. .. image:: ./images/11_hypervisor2.svg :align: center In alternativa si può utilizzare un **virtual machine monitor**, ovvero uno strato software che scherma completamente l'hardware sottostante, effettuando un completo disaccoppiamento tra hardware e software [#]_. Il seguente diagramma illustra questo schema. .. image:: ./images/12_hypervisor1.svg :align: center ------------------------- .. [#] NdR. Si veda ad esempio l'implementazione dell'interprete CPython, che utilizza un mutex, detto `global interpreter lock `_, per fare in modo che il controllo dell'interprete sia attuato solo da un singolo thread per volta. Si veda `What is the Python Global Interpreter Lock (GIL)? `_ per un articolo introduttivo all'argomento. .. [#] NdR. Questa è utopia. Gli amministratori di sistema devono gestire la piattaforma standard **oltre** tutti i vari sistemi operativi delle macchine virtuali. .. [#] NdR. Questi sono gli hypervisor di tipo 2: programmi che si appoggiano al sistema operativo dell'ospite. Esempi: VMware Workstation, Oracle VirtualBox, Parallels Desktop, Microsoft hyperV. .. [#] NdR. Questi sono gli hypervisor di tipo 1. Esempi: VMware ESXi e XenServer.