• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Multithreading, multiprocessing e Asincronia
 

Multithreading, multiprocessing e Asincronia

on

  • 886 views

A brief introduction to parallellization techniques on linux

A brief introduction to parallellization techniques on linux

Statistics

Views

Total Views
886
Views on SlideShare
883
Embed Views
3

Actions

Likes
0
Downloads
3
Comments
0

2 Embeds 3

http://www.steampdf.com 2
https://www.linkedin.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Multithreading, multiprocessing e Asincronia Multithreading, multiprocessing e Asincronia Presentation Transcript

    • Linux Internals:Multiprocessing, Multithreading e AsincroniaCome aumentare le prestazioni parallelizzando Sebastiano Merlino merlino.sebastiano@gmail.com
    • Perch´ parallellizzare? eMigliora le prestazioni del programma (mentre un processo ` in eattesa per esempio in fase di I/O, un altro pu` lavorare). oMigliora la distribuibilit` del programma (rendendolo pronto per ala scalabilit` orizzontale). a` spesso vitale e necessario (come nel caso delle interfaccieEgrafiche).`E utile a volte persino in termini di modularit` e leggibilit` del a acodice.
    • MultiprocessingCon multiprocessing, in questo contesto, intendiamol’esecuzione di multipli processi software concorrenti cheabbiano spazi di memoria indipendenti.I processi su Linux proliferano mediante meccanismo digenerazione. Il processo di sistema operativo che si occupa dellagenerazione di tutti i processi kernel necessari ` init. Ogni eprocesso ` identificato da un PID univoco. eIl meccanismo scelto su linux per la generazione di nuoviprocessi ` la cosiddetta fork utilizzando la quale un processo esdoppia il proprio spazio di indirizzi in memoria permetterndol’esecuzione di un nuovo processo che sfrutti questa copia.
    • Fork La funzione fork (int fork()) trasforma un singolo processo in esecuzione in due processi identici, noti come parent e child. Quando l’esecuzione ha successo, la fork ritorna 0 al processo child e il PID del child al processo parent. #include <unistd.h> #include <stdio.h> int main() { int cpid = fork(); if(cpid > 0) { printf("I am the father and this is my child: %dn", cpid); } else if (cpid == 0) { printf("I am the childn"); } else { printf("Error!"); }}
    • Meccanismi di IPC`E spesso necessario rendere possibile ai processi creati ilcomunicare fra loro.I sistemi di IPC (Inter-process communication) che Linuxfornisce sono svariati: Pipe (< stdio.h >) Signal (< signal.h >) Message Queues (< sys/msg .h >) Semaphores (< sys/sem.h >) Shared Memory (< sys/shm.h >) Socket (< sys/socket.h >)
    • PipeLe Pipe sono oggetti grazie ai quali l’output di un processo pu` oessere iniettato in input ad un altro processo.Linux fornisce due modi di definire pipe: 1. FILE* popen(char* command, char* type) - apre una pipe ove command ` il processo con il quale ci vogliamo connettere. e Il tipo pu` essere “r” in lettura o “w” in scrittura. Le pipe cos` o ı aperte vanno chiuse tramite pclose(FILE* fs) e vengono scritte/lette tramite l’utilizzo di fprintf e fscanf. 2. int pipe(int fd[2]) - crea una pipe restituendo due file descriptor. fd[0] ` aperto in lettura ed fd[1] ` aperto in e e scrittura. Le pipe vanno sempre chiuse mediante metodo close(int fd). Vengono sfruttati i metodi read e write.
    • #include <signal.h>#include <stdio.h>int main() { int pdes[2]; pipe(pdes); if(fork() == 0) { char received_message[10]; close(pdes[1]); read(pdes[0], (void*) received_message, 9); printf("message received: %sn", received_message); } else { close(pdes[0]); write(pdes[1], (void*) "Hello!", 7); printf("message sent: Hello!n"); } return 0;}
    • SignalI segnali sono interrupt generati via software che possono essereinviati ad un processo per segnalargli che ` avvenuto un evento. eI segnali possono essere generati in maniera sincrona, ma sonogenealmente meccanismi asincroni. Essi sono identificatiunivocamente da un numero intero compreso tra 0 e 31.Due sono i modi pi` comuni per l’invio di un segnale: u int kill(int pid, int signal) - ` una chiamata di sistema che e permette di emettere un segnale. Se il pid ` maggiore di zero, il e processo da esso identificato sar` l’unico destinatario; nel caso a in cui il pid sia zero, il messaggio sar` inviato a tutti. a int raise(int signal) - invia un messaggio al processo running. Utilizza kill in maniera trasparente ponendo come pid quello del processo che la esegue.
    • SignalUn’applicazione pu` definire una funzione cosiddetta che verr` o ainvocata quando uno specifico segnale viene ricevuto.L’handler per i segnali viene definito tramite la chiamata int(*signal(int sig, void (*func)()))(); ricevuto il segnaleidentificato da sig, la funzione func verr` invocata. func pu` a oassumere tre valori: SIG DFL - funzione di default; il processo sar` terminato una a volta ricevuto il segnale SIG IGN - funzione ignore; il segnale verr` catturato ed ignorato a un generico puntatore a funzione.
    • #include <stdio.h>#include <signal.h>void sigproc(void);void quitproc(void);main(){ signal(SIGINT, sigproc); signal(SIGQUIT, quitproc); printf(‘‘ctrl-c disabled use ctrl-$backslash$$backslash$ to quit$backslash$n’’); for(;;); /* infinite loop */}void sigproc(){ signal(SIGINT, sigproc); /* */ /* NOTE some versions of UNIX will reset signal to default after each call. So for portability reset signal each time */ printf(‘‘you have pressed ctrl-c $backslash$n’’);}void quitproc(){ printf(‘‘ctrl-$backslash$$backslash$ pressed to quit$backslash$n’’); exit(0); /* normal exit status */}
    • Message QueuesLe code di messaggi si basano su uno schema molto semplice.Un processo piazza un messaggio all’interno di una struttura(una coda) fornita dal sistema operativo. Un altro processo,conoscendo l’identificativo della coda da cui leggere potr`asuccessivamente recuperare il messaggio.Una coda di messaggi pu` essere inizializzata mediante il ometodo int msgget(key t key, int msgflg); il metodo ritornal’identificativo univoco della coda - i parametri specificatirappresentano il nome assengato alla coda e dei flag utili per ilmanagement dei diritti.Un messaggio viene inserito nella coda tramite il metodomsgsnd e viene invece recuperato mediante msgrcv.
    • SemaphoresI semafori costituiscono un fondamentale sistema di coordinamentotra processi.Qualora due o pi` processi condividano la stessa risorsa mediante umeccanismi di IPC ` vitale che l’accesso a tale risorsa sia ecorrettamente gestito (osserveremo meglio questo problema nellaparte dedicata ai thread).Un semaforo viene inizializzato (o se ne ottiene accesso) tramite: intsemget(key t key, int nsems, int semflg); ove, key rappresenta unnome associato all’id univoco che la funzione ritorna; nsems indica ilnumero di semafori che si vogliono ottenere e semflg sono i flag digestione assegnati al semaforo.Operazioni sul semaforo vengono eseguite mediante la direttiva intsemop(int semid, struct sembuf *sops, size t nsops);
    • Shared MemoryLa shared memory rappresenta il metodo, forse pi` veloce di scambiare dati fra uprocessi differenti. In sostanza, un processo crea un’area di memoria alla qualealtri processi possono accedere (se viene loro dato diritto). Questo metodo siporta ovviamente dietro il problema della condivisione di una risorsa e pertantoporta all’insorgenza della necessit` di sincronizzare l’accesso da parte dei processi aalla memoria condivisa.Un segmento di memoria condivisa ` acceduto tramite int shmget(key t key, size t esize, int shmflg);Per potere usare la memoria condivisa, il processo deve farne attach al suo spaziodi indirizzi; questo viene fatto tramite void *shmat(int shmid, const void*shmaddr, int shmflg); che ritorner` in shmaddr l’indirizzo alla head della amemoria condivisa.Il processo rilascia l’accesso tramite int shmdt(const void *shmaddr);Trascuro per ora le problematiche di sincronizzazione, essendo queste simili permolti versi a quelle successivamente trattate nella parte dedicata ai thread.
    • SocketLe Socket costituiscono un sistema di IPC punto-punto e bidirezione. Purnon essendo il sistema pi` veloce di comunicazione, la loro versatilit` e la u apossibilit` di usarle attraverso la rete le rendono molto usate. aEsistono 4 tipi di socket: Stream Socket (SOCK STREAM) - bidirezionali, sequenziali, affidabili e non duplicati senza limiti di trasmissione. Usa TCP. Datagram Socket (SOCK DGRAM) - bidirezionali, non assicurano l’ordine dei messaggi che sono costituiti da pacchetti di dimensione finita. Non necessita di connessione. Usa UDP Sequential Packet Socket (SOCK SEQPACKET) - essenzialmente un Datagram ma sequenziale, affidabile e con connessione. Mai implementato. Raw - utilizza direttamente i protocolli di comunicazione.
    • SocketLe Socket vengono create mediante la funzione int socket(int domain, int type, intprotocol) (se il protocollo non ` specificato esso sar` il default per il tipo). Le Socket si e acollegano le une alle altre tramite indirizzi. Per assegnare un indirizzo ad una socket siusa il metodo int socket(int domain, int type, int protocol).Le connessioni tramite Socket sono solitamente non bilanciate (client-server). Il server(in una connessione STREAM) chiama int listen(int s, int backlog) ove backlograppresenta il numero massimo di connessioni da accodare.Il client per collegarsi al Server chiamer` int connect(int s, struct sockaddr *name, int anamelen) e, infine, il server, per accettare la connessione eseguir` int connect(int s, astruct sockaddr *name, int namelen) che restituisce il file descriptor di una socketdedicata esclusivamente alla connessione.In una socket stream, la ricezione e l’invio dei messaggi sono eseguiti mediante i metodiread() e write() o, alternativamente, recv(...) e send(...).In una socket datagram, invece si usano i metodi sendto() e recvfrom() o, in alternativa,recvmsg() e sendmsg.
    • Soluzioni differenti - OpenMPOpenMP ` una API che supporta il multiprocessing con memoria condivisa. eLa semantica della libreria ` cos` semplice da farla apparire quasi come una funzionalit` del linguaggio. e ı aLa libreria si occupa in maniera del tutto trasparente per il programmatore del partizionamento dei dati e delladistribuzione tra i core del codice.#include <stdio.h>int main(void){ #pragma omp parallel printf("Hello, world.n"); // Verr` stampato una volta per ogni processore a return 0;}int main(int argc, char *argv[]) { const int N = 100000; int i, a[N]; #pragma omp parallel for // k cicli saranno eseguiti in parallelo (con k numero di processori) for (i = 0; i < N; i++) a[i] = 2 * i; return 0;}
    • MultithreadingPossiamo considerare un Thread come un lightweight Process.L’utilizzo di soluzioni multithread ha grandi benefici: Aumenta la responsivit` - in presenza di GUI, ad esempio, a permette di dedicare un thread ad ogni attivit` grafica. a Usa in maniera pi` efficiente i sistemi multiprocessore - ` pi` u e u facile scalare rispetto al numero di processori applicazioni multithread. Migliora la struttura di programma - molti programmi risultano meglio strutturati come sistemi a parti semi-indipendenti che come monoliti.
    • Confronto tra Multiprocessing e MultithreadingI benefici dei thread rispetto ai processi consistono essenzialmente in: minor tempo per essere creati (dato che sfruttano l’address space senza copiarlo) minor tempo per essere terminati minor tempo per switchare da un thread ad un altro (rispetto a quello richiesto per switchare da un processo all’altro) minore overhead dovuto alla comunicazioneIl vantaggio dei processi sui thread `, invece rappresentato da un emaggior livello di isolamento.
    • PthreadsLa libreria pi` importante per la realizzazione di codice umultithread ` libpthread (pthread.h). ePer creare un thread si usa int pthread create(pthread t *tid,const pthread attr t *tattr, void*(*start routine)(void *),void *arg); - se non si passano attributi, il thread sar` non adetachato e erediter` la priorit` del padre. a aLa libpthread ` parecchio complessa e contiene molti metodi eper la gestione e l’inizializzazione dei thread dei quali nontratteremo qui (creare variabili thread local, gestione priorit`, aecc...).
    • SincronizzazioneLa libpthread offre anche vari meccanismi di sincronizzazione fra ithread. La sincronizzazione in questo caso ` vitale poich´ i thread e econdividono lo stesso spazio di indirizzi. Mutual exclusion locks (Mutex) Condition variable attributes Semaphores (non li tratteremo in quanto molto simili a quelli visti per i processi)
    • Sincronizzazione MutexI mutex rappresentano un sistema molto diffuso di serializzazione delleoperazioni; essi sincronizzano i thread serializzando l’accesso aporzioni del codice definite sezioni critiche.Un mutex viene creato tramite le funzione intpthread mutex init(pthread mutex t *mp, constpthread mutexattr t *mattr); - ove il passaggio degli attributi ` del etutto arbitrario.Esistono mutex di varia natura (unici, condivisi, scoped, ecc...) e, levarie tipologie, se non fornite in libreria, sono facilmente realizzabili.I mutex vengono lockati e slockati rispettivamente tramite le funzionipthread mutex lock e pthread mutex unlock.
    • Sincronizzazione Condition Variables Le Condition Variables sono usate per bloccare atomicamente un thread finch´ un e determinato evento non si verifica; esse sono sempre utilizzate in congiunzione con Mutex e lock. Si controlla la condizione dopo avere acquisito il lock e, nel caso questa non sia verificata ci si mette in wait sulla condizione mentre atomicamente si rilascia il lock. Quando un altro thread cambia la condizione e notifica il cambio si riacquisisce il lock e si rivaluta la condizione. Le Condition Variables sono inizializzate cos` int ı pthread cond init(pthread cond t *cv, const pthread condattr t *cattr); Varie versioni del metodo wait (semplice, temporizzata, ecc...) possono essere usate per attendere che la condizione si verifichi La condizione pu` essere segnalata mediante: int o pthread cond signal(pthread cond t *cv);. Il modo corretto di mettersi in attesa su una condition variable ` il seguente: epthread_mutex_lock();while(condition_is_false) // previene Spurious WakeUp pthread_cond_wait();pthread_mutex_unlock();
    • Moderne Amenit` aNegli anni, molte altre librerie che abilitano il multithreading sononate. Fra queste ritroviamo: boost::thread - contiene tutti i maggiori paradigmi di gestione dei thread e della loro sincronizzazione Qt - dispone di versioni ad alto livello di trasparenza delle funzionalit` fin qui esposte a
    • AsincroniaRisulta spesso utile non assegnare specificatamente un job ad unthread ma mantenere un pool di thread detti workers ed assegnare aduno di essi un job solo quando si verifichi uno specifico evento che lorichieda. Quest’idea ` alla base dei pattern reactor e proactor. eUn sistema di questo tipo ` detto asincrono in quanto si oppone al eclassico meccanismo sincrono basato su loop e polling o wait sucondition variable in cui un job ` assegnato sempre e solo ad un ethread (nel quale quindi, per ogni job viene creato un nuovo thread).Questo sistema ` vincente in quanto ottimizza le prestazioni del eprogramma sia in termini di cicli eseguiti che in termini di memoriaoccupata; tra l’altro esso rende la quantit` di risorse necessarie al aprogramma prevedibile in ogni momento e non strettamentedipendente dagli input.
    • AsincroniaUn problema molto sentito ` quello della gestione multithread di un esistema Thread che riceve richieste da vari client. Se le richiestefossero servite sequenzialmente, non sarebbe possibile gestire richiesteconcorrenti.La prima soluzione che ci viene in mente ` quella di creare un thread eper ciascuna richiesta ricevuta. In questo modo sicuramente riceviamoil problema di gestione ma si presenta un nuovo problema. Se, infatti,ricevessimo nello stesso istante una gran quantit` di richieste, eg. a10000, avremmo uno spawn istantaneo di 10000 thread che potrebbeportare al kill del nostro processo da parte del sistema operativo.La soluzione a questo problema (peraltro noto come c10k problem) sibasa proprio su meccanismi asincroni come quelli descrittiprecedentemente; molti di questi si basano sull’utilizzo di select epoll.
    • select e pollselect e poll sono potenti strumenti per il multiplexing delle socket. Unprogrammatore pu` usare questi strumenti per scoprire quando un evento di oscrittura sia avvenuto su una socket cos` da comprendere che ` il momento ı edi leggere.La select funziona come segue: int select(int nfds, fd set *readfds, fd set*writefds, fd set *errorfds, struct timeval *timeout); ove nfds ` il fd di evalore pi` alto, readfds, writefds e errorfds sono dei set di file descriptor da ucontrollare e timeout ` un eventuale timeout che scatta qualora non eavvengano eventi su nessun file descriptor. La funzione torna il numero difd su cui ` avvenuto un evento e riempie i set con solo i file descriptor su ecui sono avvenuti eventi.La poll ` un’evoluzione della select che ne migliora essenzialmente la esemantica. Essa permette di specificare il tipo di eventi su cui porsi inascolto (oltre a supportare un numero maggiore di eventi). Ad oggi poll() ` edeprecata e sarebbe bene usare la nuova versione detta epoll().