Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D La Corte in Rete
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Clemente Galdi » 22.Comunicazione tra processi: le Pipe. Introduzione alla programmazione multi-thread


Inter Process Comminication

Affinché sia possibile la cooperazione tra processi, è necessaria la presenza di primitive di comunicazione.
I segnali forniscono uno strumento primitivo di comunicazione.

  • Il messaggio consiste nel “numero del segnale” ed, eventualmente, le informazioni in siginfo_t.

Le primitive per Inter Process Communication (o IPC), consentono la comunicazione di messaggi arbitrari tra processi distinti.
In passato esistevano diverse primitive per IPC su sistemi Unix diversi. In molti casi, vista la stretta relazione tra kernel e primitive IPC, la stessa primitiva aveva comportamenti diversi su sistemi diversi, consentivano raramente la portabilità del codice.
Fortunatamente, l’opera di standardizzazione ha reso la situazione molto più agevole, rendendo uniforme interfacce e comportamenti delle singole primitive.
Abbiamo visto che i socket consentono la comunicazione di messaggi arbitrari tra processi che possono essere in esecuzione sullo stesso calcolatore o, persino, su su calcolatori distinti.
In questa lezione vedremo altri due strumenti di comunicazione tra processi in esecuzione sullo stesso calcolatore, pipe e fifo.

Le pipe

Le pipe (tubi) costituiscono, storicamente, la prima primitiva per IPC fornita dai sistemi Unix.
Le pipe hanno due grosse limitazioni:

  • Sono, tipicamente, half-duplex (o monodirezionali). Sebbene esistano implementazioni di pipe bi-direzionali, per garantire la portabilità, è opportuno considerare sempre le pipe come monodirezionali.
    • Un processo scrive sulla pipe (usando write)
    • Un altro processo legge dalla stessa pipe (usando read)
  • Le pipe possono essere utilizzate solo da processi che hanno un antenato comune. In genere la pipe viene creata da un processo che, successivamente, esegue una fork. La pipe viene utilizzata per la comunicazione tra padre e figlio. In ogni caso, deve essere utilizzata da (almeno) un discendente del processo che crea la pipe.

Nonostante queste limitazioni, le pipe half-duplex sono uno degli strumenti per IPC più utilizzati nei sistemi Unix.
Ad esempio, per redirigere l’output di un processo sull’input del processo che lo segue in una pipeline, la shell crea una pipe ed esegue le opportune redirezioni degli standard output/input.

Creazione di una pipe

Per creare una pipe è possibile utilizzare la seguente system call:
int pipe(int fd[2]);

Riceve come parametro un array di due interi e restituisce 0 in caso di successo, -1 altrimenti.
Crea una pipe ed assegna due file descriptor ai due end-point della pipe: uno per la lettura e uno per la scrittura.
Per cancellare una pipe, è sufficiente chiudere i descrittori con la system call close.
Consideriamo il frammento di codice:
int fd[2];
if (pipe(fd)<0) perror("Pipe"), exit(-1);

dopo la sua esecuzione:

  • fd[0] è il descrittore per leggere dalla pipe
  • fd[1] è il descrittore per scrivere sulla pipe

La fstat su uno dei due file descriptor ritorna il tipo di file FIFO.

Rappresentazione grafica di una pipe.

Rappresentazione grafica di una pipe.


Creazione di una pipe (segue)

Una pipe in un solo processo è praticamente inutile.
Tipicamente, un processo crea una pipe ed esegue la fork. Il processo figlio eredita dal padre tutti I file aperti ed, in particolare, I riferimenti ai file descriptor della pipe. Viene così creato un canale di comunicazione tra padre e figlio.
La figura in alto riporta graficamente lo stato della memoria dopo l’esecuzione della fork.
A questo punto, è necessario stabilire quale sia la direzione delle informazioni.
In caso la pipe debba essere utilizzata per trasferire informazioni dal figlio al padre:

  • il figlio chiude l’end-point in lettura;
  • il padre chiude l’end-point in scrittura.

La figura in basso riporta lo stato delle connessioni per il caso appena illustrato.

La stato della memoria dopo la fork.

La stato della memoria dopo la fork.

Lo stato delle connessioni successivamente alle close nel padre e nel figlio.

Lo stato delle connessioni successivamente alle close nel padre e nel figlio.


Creazione di una pipe (segue)

Esempio di creazione di una pipe: Mostra codice, per l'invio di messaggi dal processo figlio al processo padre.
Da rimarcare la necessità di creare la pipe prima dell'esecuzione della fork.
Il processo padre chiude il file descriptor associato all'end-point in scrittura. Ed esegue una lettura dalla pipe.
Il processo figlio chiude l'end-point in lettura e scrive un messaggio sulla pipe.

Sebbene sia possibile creare pipe "condivise" tra più processo scrittori e/o più processi lettori, tipicamente questi strumenti di comunicazione vengono utilizzati esattamente da uno scrittore ed un lettore.

Lo stato delle connessioni successivamente alle close nel padre e nel figlio.

Lo stato delle connessioni successivamente alle close nel padre e nel figlio.


Lettura e scrittura su una pipe

Una pipe si dice broken (o rotta) quando tutti i processi che la condividono hanno chiuso il file descriptor in scrittura (risp., in lettura). In altri termini, una pipe è broken quando non esistono più processi che possono scrivere (o leggere dalla pipe).
Le operazioni di lettura e scrittura su una pipe seguono le seguenti regole:

  • All’atto della sua creazione la pipe è vuota.
  • La write aggiunge dati alla pipe.
    • Se un processo scrive su una pipe rotta, riceve il segnale SIGPIPE, la cui azione di default è la terminazione del processo. Intercettando od ignorando il segnale, la write ritorna -1 e la variabile errno assume il valore EPIPE.
    • Se il numero di byte scritti sulla pipe è al più PIPE_BUF, la write si può considerare “atomica”. In caso contrario, se la pipe è ondivisa tr più scrittori concorrenti, il messaggio scritto da una singola write potrebbe essere intervallato da messaggi scritti da altri processi.
  • La read legge e rimuovedati dalla pipe
    • Non è possibile leggere più volte gli stessi dati da una pipe;
    • Non è possibile invocare una “lseek” su una pipe;
    • I dati vengono letti in ordine First In First Out;
    • Il valore di ritorno di una read eseguita su una broken pipe è pari a zero;
    • Una read eseguita su una pipe vuota (non broken) è bloccante;
    • Una read di x byte dalla pipe risulta nella lettura di (a) x byte se nella pipe vi sono almeno x byte oppure (b) y byte se nella pipe sono presenti y<x byte.

Pipe e redirezione

Uno dei problemi legati all’esecuzione di programmi implementati da terzi è l’impossibilità di modificare il codice del programma prima di eseguirlo per indicare una sorgente alternativa di input od output. Ad esempio:

  • Il comando ls visualizza l’output su standard output.
  • L’invocazione di questo comando tramite exec non consente la redirezione dello standard output.

Una delle possibilità offerte dalle pipe consiste proprio nella possibilità di redirezione dell’input e dell’output come segue:

  • Il processo padre:
    • Crea la pipe;
    • Crea il processo figlio;
    • Legge dalla pipe (risp., scrive sulla pipe).
  • Il processo figlio:
    • Redirige lo standard output/error sull’end-point in scrittura (risp., standard input sull’end-point in lettura),
    • Esegue la exec del programma da eseguire.
  • Questa strategia consente al padre di ricevere l’output del (o di inviare input al) programma eseguito dal processo figlio.
  • Padre e figlio possono (ovviamente) creare due pipe monodirezionali in modo che il padre invii “input” al programma eseguito dal figlio e legga “l’output” corrispondente.

Redirezione: Un esempio

Il seguente programma: Mostra codice, è un semplice esempio di redirezione.
Il padre crea la pipe e crea un processo figlio.
Il figlio, chiude l'endpoint in lettura della pipe e redirige lo standard output sull'end-point in scrittura della pipe.
A questo punto esegue il comando "ls" attraverso l'invocazione della execl.
Si rammenta che le system call della famiglia exec ritornano solo in caso di errore.
Il processo padre, legge dalla pipe e scrive I messaggi ricevuti sullo standard error.
In breve, questo programma implementa il comando "ls" in cui lo standard output è rediretto sullo standard error.

Redirezione: Secondo esempio

Le funzioni che invocano i comandi ls e sort: Mostra codice

Un esempio in cui la pipe è utilizzata come canale di comunicazione tra I due figli di un stesso processo.
In particolare, questo programma esegue la pipeline "ls |sort", in cui ogni comando è eseguito da uno dei figli del processo padre.

Il processo padre: Mostra codice, crea la pipe, crea entrambi I figli, chiude entrambi gli end-point della pipe.
Importante: Le funzioni figliols e figliosort NON ritornano MAI. Se la exec ha successo, il codice del programma viene sostituito dal codice dell'eseguibile indicato dalla exec, altrimenti viene eseguita la system call "exit(-1)".

  • Il primo figlio, chiude l'end-point in lettura, redirige lo standard output sull'end-point in scrittura ed esegue il comando ls.
  • Il secondo figlio, chiude l'end-point in scrittura, redirige lo standard input sull'end-point in lettura, chiude il riferimento all'end-point in lettura, ed esegue il comando sort.

Redirezione: Secondo esempio (segue)

Le funzioni che invocano i comandi ls e sort: Mostra codice
Il corpo del processo padre: Mostra codice
I riferimenti all'end-point per la scrittura sulla pipe sono 3: uno nel padre, uno nel primo figlio e uno nel secondo figlio.
D'altro canto, il comando sort "esegue" l'ordinamento una volta che ha letto dallo standard input l'end-of-file.
Da quanto detto in precedenza, la lettura da una pipe ritorna "end-of-file" solo quando tutti I processi che condividono la pipe hanno chiuso il loro end-point in scrittura.
Affinchè il programma funzioni correttamente, è necessario, quindi, chiudere tutti I riferimenti non utilizzati all'end-point in scrittura della pipe per evitare che il comando sort si blocchi in attesa di input.

Si noti che:

  • il padre ed il secondo figlio, chiudono "esplicitamente" il proprio riferimento ad fd[1];
  • il primo figlio chiude "implicitamente" fd[1] all'atto della su terminazione.

Questa condizione di "attesa" imposta dalla semantica delle pipe, garantisce, infine, che sort ordinerà l'intero output di ls, dato che non inizierà l'ordinamento prima della terminazione del processo "ls".

FIFO

Una delle restrizioni legate all’utilizzo delle pipe è che condizione necessaria affinché due processi possano utilizzare questo strumento è avere un antenato comune che crea la pipe.
Le FIFO, anche note come named pipe, consentono anche a processi tra cui non esiste una relazione, di comunicare utilizzando uno strumento del tutto simile alle pipe.
La creazione di una FIFO avviene utilizzando la seguente system call:
int mkfifo(const char *pathname, mode_t mode);

La system call mkfifo è molto simile alla creat. Difatti il primo parametro identifica il pathname della fifo mentre il secondo definisce le protezioni su di essa.
Questa system call crea un file speciale sul file system, identificato dal pathname, il cui “tipo fi file” definito nel campo st_mode della struttura stat è, appunto, FIFO.

  • Dopo la creazione, la FIFO può essere usata come “un file”
    • utilizzando open, read, write, close
    • Come per le pipe, NON è possibile utilizzare la lseek
  • L’apertura della FIFO in sola lettura (risp., in sola scrittura) è bloccante fino a quando un altro processo non apra la FIFO in scrittura (risp., lettura).
  • È comune avere più processi utilizzano la stessa FIFO in scrittura.
    • Se il numero di byte scritti sulla FIFO da una singola write è inferiore a PIPE_BUF, la scrittura è “atomica”.

Lettura e scrittura su FIFO

Come nel caso delle pipe, una FIFO è broken quando non esistono processi per cui la fifo è aperta in lettura (resp., scrittura).
A differenza delle pipe, le FIFO sono inizialmente broken. Difatti, quando un processo crea una pipe, entrambi gli end-point sono “automaticamente” aperti. Ciò non è vero per le FIFO in cui i processi lettori/scrittori non sono tra loro relati. In questo caso un processo potrebbe aprire una FIFO in lettura prima che un altro processo l’abbia aperta in scrittura.
Per default, quindi, la open su una FIFO può bloccare il processo che la esegue. È possibile modificare questo comportamento indicando il flag O_NONBLOCK alla open. In questo caso è necessario tenere conto del valore di ritorno della open.
Come nel caso delle pipe:

  • La scrittura su una fifo broken genera un segnale SIGPIPE per il processo che esegue l write.
  • La lettura da una fifo broken ritorna end-of-file al processo che esegue la read.

Le FIFO possono essere utilizzate in architetture client-server per consentire a più client (scrittori) di inviare richieste all’unico server (lettore).
In questo caso il server crea una FIFO, detta “well-known”, nota a tutti I client. Ogni client crea una propria FIFO specifica utilizzata dal server per l’invio delle risposte al client.

In questo caso, per evitare che il server si blocchi in attesa che un lettore apra la FIFO in scrittura, il server può:

  • Aprire la FIFO con l’opzione O_NONBLOCK;
  • Aprire la FIFO in read/write. In questo modo, la FIFO ha sempre un riferimento.

La programmazione multi-thread

Un processo può essere visto come l’esecuzione di una sequenza di istruzioni. Il processo esegue “una sola cosa per volta”. Questa strategia di esecuzione:

  • Semplifica la progettazione del software in un contesto “sequenziale” e/o “sincrono”.
    • Per “sequenziale” intendiamo una sequenza di task da eseguire in un ordine prestabilito, e.g., task1 seguito da task2, seguito da task3…
    • Per “sincrono” intendiamo più task uguali eseguiti in concorrenza ed in maniera coordinata. E.g., n>1 istanze del task1 comunicano con un server e vengono eseguite “concorrentemente”. La loro coordinazione, però, garantisce che tutte le istanze, richiedano una informazione al server “nello stesso momento”.
    • In ogni momento, il progettista sa esattamente cosa farà il software e “dove” si trova l’esecuzione.
  • Riduce le performance del sistema. Ad es., una operazione di I/O bloccante, congela l’esecuzione del processo. Inoltre, se anche esistessero altre operazioni, potenzialmente eseguibili, anch’esse risulterebbero bloccate dall’attesa dell’I/O.
  • Complica la progettazione del software in un contesto “asincrono”. Si pensi, ad esempio, ad un server che deve gestire richieste provenienti da client diversi ed indipendenti. In questo caso, diverse richieste, che possono richiedere task diversi al server, possono giungere da client diversi in momenti arbitrati.

La programmazione multi-thread (segue)

Molti sistemi operativi moderni consentono la possibilità di eseguire, all’interno di uno stesso processo, più percorsi di esecuzione. Il “processo”, può eseguire più task contemporaneamente avendo accesso agli stessi dati.
Ogni percorso di esecuzione è noto come thread ed un processo “classico” può essere visto come una applicazione “multi-thread con un unico thread di controllo”.
La programmazione multi-thread consente di:

  • Semplificare la progettazione del codice in un contesto asincrono. È sufficiente assegnare un thread ad ogni evento. Ogni thread può, quindi, gestire in modo “sincrono” il proprio evento.
  • Semplificare la condivisione delle informazioni. I thread sono parte dello stesso processo e, quindi, condividono la “memoria”. La comunicazione tra thread diversi, quindi, non richiede l’utilizzo di strumenti quali pipe, fifo, socket od altro.
  • Migliorare le performance dell’intero sistema. Il blocco di un thread a causa di, ad esempio, una operazione I/O, NON blocca gli altri thread del processo.

I thread vengono eseguiti nel contesto del processo e la terminazione del processo implica la terminazione di tutti I suoi thread di controllo.

Thread e condivisione dei dati

Nei sistemi Unix ad ogni processo sono associate una serie di informazioni di controllo, tra cui ricordiamo:

  • Pid, pid del padre, user id, group id;
  • Ambiente, La directory corrente;
  • Il codice del programma, lo stack, I registri;
  • L’heap per la memoria dinamica, I descrittori dei file aperti;
  • Informazioni associate ai segnali: segnali bloccati, pendenti, azioni.

Ogni thread possiede i propri:

  • Stack (nota: le variabili locali vengono memorizzate sullo stack);
  • Registri;
  • Insiemi dei segnali pendenti e bloccati;
  • Thread specific data (che tratteremo nella lezione n° 24).

Thread e condivisione dei dati (segue)

“In breve” possiamo affermare che:

  • I thread della stessa applicazione condividono, le variabili globali, I file aperti e le azioni associate ai segnali. Una modifica effettuata da un thread ad uno di queste risorse è immediatamente visibile agli altri thread della stessa applicazione;
  • I thread della stessa applicazione NON condividono le variabili locali.

L’interfaccia che utilizzeremo per I thread è nota come pthread o POSIX thread.
Visto che i thread condividono la memoria, non è possibile utilizzare una variabile globale (come errno) per i codici d’errore. Le funzioni pthread restituiscono direttamente un codice d’errore.

Identificazione dei thread

Ad ogni processo il sistema operativo assegna un identificativo, il pid, univoco all’interno del sistema.
Allo stesso modo, ad ogni thread viene assegnato un identificativo, il thread_id o tid. A differenza del pid, però, il tid è “univoco” all’interno dell’applicazione.
Un tid è rappresentato dal tipo di dato pthread_t che è dipendente dallo sistema operativo.
La libreria pthread definisce le seguenti funzioni:

pthread_t pthread_self(void);
Ritorna il thread id del thread che la esegue

Essendo la struttura dati che rappresenta un tid system-dependent NON esiste un’unica funzione per verificare se due thread id sono identici.
Lo standard POSIX definisce la seguente interfaccia:
int pthread_equal(pthread_t t1, pthread_t t2);

Questa funzione ritorna un valore diverso da zero se i tid passati come argomento sono uguali, zero altrimenti.

Creazione di thread

Come abbiamo accennato, una applicazione può essere vista come un processo che esegue un unico thread di controllo.
La creazione di un nuovo thread all’interno dell’applicazione corrente viene effettuata utilizzando la seguente funzione:
int pthread_create(pthread_t *tid, const pthread_attr_t *attributes, void *(* start)(void *), void *argument);

Dove:

  • *tid = È un argomento di ritorno. Conterrà il tid del nuovo thread
  • *attributes = attributi del thread
  • *start = La funzione che eseguirà il thread appena creato. La funzione start prende un unico parametro di tipo (void *).
  • *argument = l’argomento passato alla funzione start.

La funzione pthread_create ritorna 0 in caso di successo od un codice d’errore altrimenti.
In caso sia necessario passare al thread più parametri, è possibile creare una struttura dati e passare al thread il puntatore ad essa.
Quando il nuovo thread viene creato, eredita dal thread creatore la maschera dei segnali. Il nuovo thread, non eredita i segnali pendenti.

Terminazione di thread

Un thread “vive” nel contesto del processo. In un qualsiasi momento, l’invocazione della system call exit() provoca la terminazione dell’intero processo, I.e., di tutti I thread dell’applicazione.
Un thread può terminare in tre modi:

  • Invocando return(). Il valore di ritorno costituirà l’exit code del thread;
  • A seguito dell’invocazione di pthread_cancel da parte di un altro thread;
  • Invocando pthread_exit.

La firma di pthread_exit è la seguente: void pthread_exit(void *ret);
Termina il thread corrente, con valore di uscita ret.
Altri thread possono leggere il valore di uscita usando pthread_join.
È necessario che i dati puntati da ret sopravvivano alla terminazione del thread!
ret non può puntare alle variabili locali del thread.
È possibile utilizzare variabili globali o allocate dinamicamente.
La firma di pthread_cancel è la seguente: int pthread_cancel(pthread_t tid);
Richiede la terminazione del thread specificato da tid, non aspetta la terminazione, restituisce 0 se OK, un codice d’errore altrimenti il valore di uscita di un thread cancellato è dato dalla costante THREAD_CANCELED.

Aspettare la terminazione di thread

Un thread può attendere la terminazione di un altro thread utilizzando la seguente funzione:

int pthread_join(pthread_t tid, void **ret);

La pthread_join attende che il thread specificato da tid termini

  • se il thread è già terminato, a pthread_join ritorna immediatamente (come la system call wait)

Restituisce 0 la join ha avuto successo, un codice d’errore altrimenti;
ret è un parametro di ritorno usato per restituire il valore d’uscita dell’altro thread..Attenzione al doppio puntatore!
È possibile passare null come secondo parametro della join nel caso in cui non si è interessati all’exit value del thread.

I thread: un esempio

Le prossime slide illustrano un esempio di utilizzo di applicazioni multi-thread.
La struttura foo contiene due variabili intere.

Viene dichiarata una variabile globale test di tipi myfoo.

La routine di servizion stampa riceve come parametri una stringa ed un puntatore ad una variabile di tipo myfoo e visualizza la stringa, il tid del thread corrente ed I valori delle variabili intere compoenenti la struttura.

La funzione fun1 riceve come parametro un puntatore void *:

  • dichiara una variabile locale di tipo myfoo ed assegna valori ai campi di myfoo.
  • visualizza l’argomento ed il tid del thread corrente.
  • invoca la funzione stampa.
  • termina restituendo il puntatore alla variabile locale (come detto, questo è un ERRORE perchè le variabili locali vengono deallocate all’atto della terminazione del thread).

La struttura myfoo e la routine stampa: Mostra codice
La funzione fun1: Mostra codice

I thread: un esempio (segue)

La funzione fun2: Mostra codice riceve come parametro un puntatore void *:

  • assegna valori alla variabile globale;
  • visualizza l'argomento ed il tid del thread corrente;
  • invoca la funzione stampa;
  • termina restituendo il puntatore alla variabile globale.

La funzione fun3: Mostra codice riceve come parametro un puntatore void *:

  • dichiara un puntatore ad una struttura myfoo;
  • alloca spazione per una variabile di tipo myfoo;
  • assegna valori alla variabile allocata dinamicamete;
  • visualizza l'argomento ed il tid del thread corrente;
  • invoca la funzione stampa;
  • termina restituendo il puntatore alla variabile allocata dinamicamente.

I thread: passaggio di parametri

Il passaggio dei parametri all’atto della creazione di un nuovo thread è eseguito per riferimento. Quindi il thread creatore ed il nuovo thread condividono l’area di memoria contenente i parametri.
Ne consegue che una modifica fatta ai “parametri” da parte del thread creatore si ripercuote immediatamente sul valore dei parametri nel thread creato.
Inoltre, a meno di sincronizzazione esplicita, NON è possibile sapere quale sarà lo scheduling di esecuzione dei due thread.
È necessario, quindi, evitare modifiche dei parametri da parte del thread creatore.
Esempio di ERRORE classico: Mostra codice.
Tutti i thread aspettano 1 secondo prima di visualizzare il proprio tid ed il valore del parametro.
Il puntatore alla variabile i, indice del ciclo, viene passata come parametro per i thread. Tutti I thread avranno come parametro "la stessa area di memoria".
L'incremento di questa variabile è, quindi, immediatamente visibile a tutti I thread. Al termine del primo ciclo, i assume valore 10 e, nell'esempio, tutti I thread sono ancora in sleep. Per cui, all'atto del loro risveglio, visualizzano lo stesso valore.
L'output del programma: Mostra codice.

I thread: passaggio di parametri (segue)

Nota: La presenza dell’istruzione “sleep(1)” in fun() è puramente didattica ed ha lo scopo di “simulare” quello che potrebbe succedere in una condizione operativa normale.
NON SI ASSUMA MAI, che “in un tempo x è possibile creare y thread”.
L’affermazione può sembrare sicuramente “ragionevole”.
Ma, i tempi di effettiva creazione dei thread dipendono dal carico macchina e dallo scheduling. Esistono affermazioni “ragionevoli” che possono risultare false in alcuni contesti.
Ad esempio, su un processore particolarmente carico, la creazione di 10 thread potrebbe richiedere più di 1 secondo!
Visto che il problema nasce dal fatto che il parametro passato ai diversi thread è memorizzato nella stessa area di memoria, la soluzione (ovvia) è eliminare questa coincidenza.
Di seguito una prima possibilità: Mostra codice.
Il master thread utilizza un array (di interi in questo caso) dove, per ogni thread, esiste una locazione che memorizza il valore del parametro associato.
Chiaramente queste soluzione è percorribile nel momento in cui è noto a priori il numero di thread da creare.

L'output del programma: Mostra codice.

I thread: passaggio di parametri (segue)

Una seconda possibilità è: Mostra codice.
Il programma utilizza un unico puntatore allocato dinamicamente.
All'atto della crazione di un nuovo thread, viene allocata ("nuova") memoria per contenere il parametro da passare al nuovo thread.
Ora sebbene il puntatore al parametro da passare al thread sia unico, l'area di memoria da esso indirizzata cambia ad ogni invocazione della malloc, garantendo, quindi, che diversi thread riceveranno parametri memorizzati in aree di memoria diverse.

L'output del programma: Mostra codice

I materiali di supporto della lezione

W.R. Stevens – S.A. Rago, Advanced Programming in the Unix Environment

Capitolo 11 (11.1/11.5), Capitolo 15 (15.1, 15.2, 15.5).

POSIX Threads Programming

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion

Fatal error: Call to undefined function federicaDebug() in /usr/local/apache/htdocs/html/footer.php on line 93