· Andrea Pollini · materiale didattico · 17 min read

Processi e thread

processi, thread e loro gestione in python.

processi, thread  e loro gestione in python.

I processi furono introdotti nei sistemi operativi non appena vi furono i miglioramenti hardware che consentirono di gestire in modo accettabile i processi, senza provocare un consumo percentuale troppo elevato della CPU.

Il sistema operativo sappiamo infatti che si occupa di gestire tutti i sottosistemi (processi, memoria, comunicazioni di rete e in generale con le periferiche di I/O) e questa gestione occupa una fetta dei cicli di CPU disponibili. Ogni evoluzione dell’hardware consente al sistema operativo di avere a disposizione una quantità di cicli di clock maggiore e questo viene speso in parte per avere più potenza di calcolo da dare agli utenti ed in parte per avere dei sistemi di gestione più complessi che portino ad una gestione più efficace.

[!NOTE] Se aumentano le istruzioni a disposizione per un secondo di CPU… Se la gestione dei processi dovesse costare 1000 cicli di clock ad esempio questo nel caso di un sistema dove vengono eseguiti 1k istruzioni al secondo ha un impatto dell’1%, se le istruzioni a disposizione diventassero 100k al secondo l’impatto diventerebbe solo dello 0.001%

I processi

I processi sono stati introdotti nei sistemi operativi per gestire in modo efficiente le risorse del computer e permettere l’esecuzione di più programmi contemporaneamente. Prima dell’introduzione dei processi, ogni programma doveva essere eseguito singolarmente e in sequenza, il che rendeva difficile utilizzare il computer per più compiti contemporaneamente.

L’introduzione dei processi ha permesso ai sistemi operativi di gestire più programmi contemporaneamente, assegnando loro risorse come la CPU e la memoria in modo equo. In questo modo, i processi possono essere eseguiti in parallelo o in serie, a seconda delle risorse disponibili e delle priorità dei processi.

Inoltre, l’uso dei processi ha reso possibile l’esecuzione di programmi più complessi, che richiedono la collaborazione di più sottoprocessi per completare un compito. Ad esempio, un programma di elaborazione di immagini potrebbe suddividere il lavoro in più sottoprocessi, ciascuno responsabile dell’elaborazione di una porzione dell’immagine.

Ogni processo ha il proprio spazio di indirizzamento della memoria, risorse aperte e stato di esecuzione. I processi possono essere creati, sospesi, ripresi ed eliminati durante l’esecuzione del sistema operativo.

Una possibilità interessante quando si utilizzano i processi è quella di sfruttare tecniche di gestione di più processi contemporaneamente, quali multiprogrammazione e multitasking.

multiprogrammazione

La multiprogrammazione è una tecnica utilizzata dai sistemi operativi per gestire più processi contemporaneamente. In un sistema a multiprogrammazione, il sistema operativo tiene traccia di più processi in esecuzione e assegna loro risorse come la CPU e la memoria in modo equo. Ciò consente al sistema operativo di passare da un processo all’altro rapidamente, dando l’impressione che i processi vengano eseguiti contemporaneamente.

multitasking

Il multitasking è una caratteristica dei sistemi operativi che permette all’utente di eseguire più compiti contemporaneamente. Ad esempio, l’utente può avere in esecuzione un programma di elaborazione di testo mentre ascolta musica o scarica file da Internet. In realtà, il sistema operativo sta gestendo più processi contemporaneamente, ma li sta eseguendo uno alla volta sulla CPU. Grazie al multitasking, l’utente ha l’impressione che i compiti vengano eseguiti contemporaneamente.

Gli stati di un processo e la gestione con python

Lo stato di un processo rappresenta il suo status corrente all’interno del sistema operativo. I processi possono essere in uno dei seguenti stati:

  • Nuovo: il processo è stato creato ma non è ancora pronto per l’esecuzione.
  • Pronto: il processo è pronto per l’esecuzione sulla CPU e sta aspettando che il sistema operativo lo selezioni.
  • In esecuzione: il processo sta utilizzando la CPU per eseguire il proprio codice.
  • In attesa: il processo ha sospeso la propria esecuzione in attesa di un evento esterno, come l’apertura di un file o la ricezione di un segnale.
  • Terminato: il processo ha completato la sua esecuzione e sta per essere eliminato dal sistema operativo.

In Python, è possibile utilizzare la libreria multiprocessing per creare nuovi processi e gestire il loro stato. Ecco un esempio di come si può creare un nuovo processo e variarne lo stato:

import multiprocessing
import time
def worker():
print("Processo in esecuzione") # Simuliamo l'esecuzione del processo con una breve pausa
time.sleep(2)
print("Processo terminato")
if __name__ == "__main__":
# Creiamo un nuovo processo e lo avviamo
p = multiprocessing.Process(target=worker)
p.start()
# Verificiamo lo stato del processo
while True:
if p.is_alive():
print("Il processo è in esecuzione")
else:
print("Il processo si è terminato")
break # Simuliamo l'esecuzione del processo principale con una breve pausa
time.sleep(1)

In questo esempio, creiamo un nuovo processo utilizzando la classe Process della libreria multiprocessing. La funzione worker() viene eseguita dal processo creato. Utilizziamo il metodo is_alive() per verificare lo stato del processo e stampiamo un messaggio appropriato. Alla fine dell’esecuzione, il processo si termina e il suo stato diventa “terminato”.

I thread

I thread sono unità esecutive all’interno di un processo. In altre parole, un processo può contenere uno o più thread, che rappresentano sequenze indipendenti di istruzioni eseguibili contemporaneamente.

Abbiamo visto che un processo è rappresentato da un process control block (PCB) all’interno del sistema operativo. Allo stesso modo un thread verrà rappresentato ad un thread control block (TCB)

il thread control block e il process control block

Il Thread Control Block (TCB) e il Process Control Block (PCB) sono entrambi dati di struttura utilizzati dai sistemi operativi per gestire i thread e i processi rispettivamente. Il PCB contiene informazioni sullo stato del processo, come ad esempio l’ID del processo, le risorse allocate, lo stato del processo (ad esempio in esecuzione, sospeso, pronto), il programma di avvio e la tabella dei pagine della memoria.

Il TCB, invece, è una struttura dati simile al PCB ma specifica per i thread. Contiene informazioni sullo stato del thread, come ad esempio l’ID del thread, lo stato del thread (ad esempio in esecuzione, sospeso, pronto), il puntatore allo stack del thread e il contesto di esecuzione del thread.

In generale, il TCB è una sotto-struttura del PCB. Ciò significa che ogni processo ha un proprio PCB e tutti i thread all’interno del processo condividono lo stesso PCB ma hanno ciascuno il proprio TCB. In questo modo, il sistema operativo può gestire efficacemente le risorse tra i thread di uno stesso processo e allo stesso tempo tenere traccia dello stato di ogni singolo thread. Inoltre, il TCB contiene anche informazioni sulla priorità del thread, che viene utilizzata dal sistema operativo per decidere quale thread eseguire quando più thread sono pronti per l’esecuzione. La priorità dei thread può essere modificata dinamicamente durante l’esecuzione del programma, ad esempio in base al carico di lavoro o alle esigenze dell’applicazione.

![[Pasted image 20240811111605.png]]

Creare un thread in python

In Python, è possibile creare un thread utilizzando la classe threading.Thread. Ecco un esempio di come si può creare un thread in Python:

import threading
def funzione_thread():
print("Il thread sta eseguendo il suo lavoro")
# Creazione del thread
thread = threading.Thread(target=funzione_thread)
# Avvio del thread
thread.start()
# Attesa per l'esecuzione del thread (opzionale)
thread.join()

In questo esempio, la funzione funzione_thread viene eseguita in un thread separato quando si chiama il metodo start() sulla instancia della classe Thread. Il metodo join() viene utilizzato per attendere che il thread abbia terminato l’esecuzione prima di continuare con il resto del programma.


Il process control block (PCB)

Il Process Control Block (PCB) è una struttura dati utilizzata dai sistemi operativi per tenere traccia delle informazioni relative ad ogni processo attivo nel sistema. Il PCB contiene informazioni sullo stato del processo, come l’ID del processo, le risorse allocate, lo stato del processo (ad esempio in esecuzione, sospeso, pronto), il programma di avvio e la tabella dei pagine della memoria.

Ecco un esempio di come potrebbe essere strutturato un PCB:

struct pcb {
int pid; // ID del processo
int state; // stato del processo (es. in esecuzione, sospeso)
int priority; // priorità del processo
struct mem_block *mem_table; // tabella delle pagine della memoria allocate al processo
struct cpu_context *cpu_context; // contesto di esecuzione del processo sulla CPU
struct file_descriptor *fd_table; // tabella dei descrittori dei file aperti dal processo
struct thread_list *thread_list; // lista dei thread appartenenti al processo
};

I prerequisiti per capire la struttura del PCB sono:

  • Conoscenza delle basi dei sistemi operativi, in particolare della gestione dei processi e dei thread.
  • Comprensione di come i sistemi operativi utilizzano le strutture dati per tenere traccia dello stato dei processi e delle risorse allocate.
  • Conoscenza delle principali informazioni che devono essere tenute traccia per ogni processo attivo nel sistema, come l’ID del processo, lo stato del processo, la priorità del processo e le risorse allocate.

Il thread control block (TCB)

Il Thread Control Block (TCB) è una struttura dati utilizzata dai sistemi operativi per tenere traccia delle informazioni relative ad ogni thread attivo nel sistema. Il TCB contiene informazioni sullo stato del thread, come l’ID del thread, lo stato del thread (ad esempio in esecuzione, sospeso, pronto), il puntatore al stack del thread e il contesto di esecuzione del thread.

Ecco un esempio di come potrebbe essere strutturato un TCB:

struct tcb {
int tid; // ID del thread
int state; // stato del thread (es. in esecuzione, sospeso)
int priority; // priorità del thread
void *stack_ptr; // puntatore allo stack del thread
struct cpu_context *cpu_context; // contesto di esecuzione del thread sulla CPU
};

I prerequisiti per capire la struttura del TCB sono:

Le principali informazioni che devono essere tenute traccia per ogni thread attivo nel sistema sono:

  • ID del thread: un identificatore univoco assegnato al thread dal sistema operativo per distinguerlo dagli altri thread.
  • Stato del thread: indica lo stato di esecuzione del thread, ad esempio se è in esecuzione, sospeso o pronto per l’esecuzione. Questa informazione viene utilizzata dal sistema operativo per decidere quale thread eseguire quando più thread sono pronti per l’esecuzione.
  • Priorità del thread: indica la priorità relativa del thread rispetto agli altri thread nel sistema. La priorità viene utilizzata dal sistema operativo per decidere quale thread eseguire quando più thread sono pronti per l’esecuzione.
  • Puntatore allo stack del thread: indica l’indirizzo di memoria dell’ultima istruzione eseguita dal thread. Questa informazione viene utilizzata dal sistema operativo per ripristinare lo stato del thread quando viene sospeso e ripreso in esecuzione.

Inoltre, il TCB può contenere altre informazioni utili come ad esempio il contesto di esecuzione del thread sulla CPU, che include le istruzioni della CPU e i registri del thread.

I sistemi operativi utilizzano anche altre strutture dati per tenere traccia delle risorse allocate ai thread. Ad esempio, possono utilizzare una tabella di allocazione della memoria per tenere traccia dei blocchi di memoria assegnati ai thread e una tabella di gestione dei file per tenere traccia dei file aperti dai thread.

In generale, i sistemi operativi utilizzano queste strutture dati per monitorare lo stato dei thread e delle risorse allocate in modo da poter gestire efficacemente l’esecuzione dei programmi e garantire che le risorse siano utilizzate in modo corretto ed efficiente.


comunicazione tra processi

La comunicazione tra processi è una funzionalità importante dei sistemi operativi che permette ai processi di scambiare informazioni e dati tra loro. Ci sono diverse tecniche per la comunicazione tra processi, come ad esempio le pipe, i socket e i file condivisi.

Le pipe

Le pipe (o tubi) sono una tecnica di comunicazione tra processi che permette ai processi di scambiare informazioni attraverso un canale unidirezionale. Una pipe consiste in un buffer di memoria condivisa che può contenere dati da inviare da un processo a un altro.

Le pipe possono essere utilizzate per la comunicazione tra due processi in questo modo:

  1. Il primo processo crea una pipe e ne restituisce un’estremità (chiamata “estremità di scrittura”) al secondo processo.
  2. Il secondo processo utilizza l’estremità di scrittura per scrivere i dati nella pipe.
  3. Il primo processo utilizza l’altra estremità della pipe (chiamata “estremità di lettura”) per leggere i dati scritti dal secondo processo.

In questo modo, il secondo processo può inviare informazioni al primo processo attraverso la pipe.

Le pipe sono utili in situazioni in cui due processi devono scambiare informazioni tra loro, ad esempio in un’applicazione client-server o in una pipeline di elaborazione dei dati. Tuttavia, le pipe hanno alcune limitazioni, come il fatto che possono essere utilizzate solo per la comunicazione da un processo all’altro e non viceversa, e che i dati scritti nella pipe devono essere letti nell’ordine in cui sono stati scritti.

I socket

I socket sono una tecnica di comunicazione tra processi che permette ai processi di scambiare informazioni attraverso un canale bidirezionale. Un socket è un punto di accesso a una rete, ovvero un endpoint di comunicazione tra due processi.

I socket possono essere utilizzati per la comunicazione tra due processi in questo modo:

  1. Il primo processo crea un socket e lo lega ad un indirizzo IP e una porta specifica.
  2. Il secondo processo si connette al socket del primo processo utilizzando l’indirizzo IP e la porta specifica.
  3. Una volta stabilita la connessione, i due processi possono scambiare informazioni attraverso il socket in modo bidirezionale.

In Python, il modulo socket fornisce una varietà di funzioni per la creazione e la gestione dei socket. Ecco un esempio di come creare un socket server che accetta connessioni e invia messaggi ai client connessi:

import socket
# Create a socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Bind the socket to an address and port
s.bind(('localhost', 12345))
# Listen for incoming connections
s.listen()
while True:
# Accept a connection from a client
c, addr = s.accept()
print('Got connection from', addr)
# Send a message to the client
c.send(b'Thank you for connecting')
# Close the connection
c.close()

Ecco invece un esempio di come creare un client che si connette al server e riceve il messaggio inviato dal server:

import socket
# Create a socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to the server at localhost on port 12345
s.connect(('localhost', 12345))
# Receive data from the server
data = s.recv(1024)
print(f'Received {data.decode()}')
# Close the connection
s.close()

In questo esempio, il client si connette al server utilizzando l’indirizzo IP localhost e la porta 12345, riceve il messaggio inviato dal server e lo stampa a schermo.

Comunicazione con file condivisi

I file condivisi (o file shared memory) sono una tecnica di comunicazione tra processi che permette ai processi di scambiare informazioni attraverso un’area di memoria condivisa. In pratica, i processi possono accedere alla stessa porzione di memoria e leggere o scrivere dati in modo sincronizzato.

I file condivisi possono essere utilizzati per la comunicazione tra due processi in questo modo:

  1. Il primo processo crea un file condiviso e ne restituisce il descrittore.
  2. Il secondo processo apre il file condiviso utilizzando il descrittore fornito dal primo processo.
  3. Entrambi i processi possono ora leggere o scrivere dati nel file condiviso.

In Python, la libreria mmap fornisce funzioni per l’accesso ai file condivisi. Ecco un esempio di come creare un server che scrive in un file condiviso e un client che legge da esso:

import mmap
# Server code
def server():
# Create a shared memory file of size 1024 bytes
f = open('shared_file', 'w+b')
f.write(b' ' * 1024)
f.seek(0)
# Map the file into memory and return the memory object
mem = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_WRITE)
# Write data to the shared memory
mem.write(b'Hello client!')
# Client code
def client():
# Open the shared memory file in read mode
f = open('shared_file', 'rb')
f.seek(0)
# Map the file into memory and return the memory object
mem = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
# Read data from the shared memory
data = mem.read(12)
print(f'Received {data.decode()}')
# Run server and client functions in separate threads
import threading
t_server = threading.Thread(target=server)
t_client = threading.Thread(target=client)
t_server.start()
t_client.start()
t_server.join()
t_client.join()

In questo esempio, il server crea un file condiviso di dimensione 1024 byte e scrive i dati “Hello client!” in esso. Il client apre lo stesso file condiviso in modalità di sola lettura e legge i dati scritti dal server. Entrambi i processi utilizzano la libreria mmap per accedere al file condiviso.

Nota che in questo esempio il file condiviso viene creato e aperto con il nome “shared_file”. Assicurati di utilizzare lo stesso nome per entrambi i processi o di modificare il codice per adattarsi alle tue esigenze.

comunicazione mediante code di messaggi

comunicazione tra processi

In Python, il modulo multiprocessing fornisce un’interfaccia per la creazione e la gestione dei processi e per la comunicazione tra di loro. Ad esempio, è possibile utilizzare una Queue per far comunicare due processi. Ecco un esempio:

from multiprocessing import Process, Queue
def fun1(q):
q.put([4, 'hello from function 1'])
def fun2(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p1 = Process(target=fun1, args=(q,)))
p2 = Process(target=fun2, args=(q,)))
p1.start()
p1.join()
p2.start()
p2.join()

In questo esempio, vengono creati due processi p1 e p2 che eseguono rispettivamente le funzioni fun1() e fun2(). La funzione fun1() inserisce un elemento nella coda q, mentre la funzione fun2() stampa l’elemento presente in testa alla coda q. In questo modo, i due processi comunicano attraverso la coda q.

In generale, la comunicazione tra processi può essere utilizzata per implementare una varietà di applicazioni, come ad esempio il calcolo distribuito o la gestione di un sistema operativo in multitasking. Ad esempio, nel calcolo distribuito, i processi possono scambiare informazioni tra loro per suddividere il carico di lavoro e accelerare i tempi di esecuzione dell’applicazione. Nella gestione di un sistema operativo in multitasking, la comunicazione tra processi può essere utilizzata per coordinare l’esecuzione di più compiti contemporaneamente. La comunicazione tra processi è un concetto fondamentale nell’ambito della programmazione parallela e del multiprocessing. Le diverse modalità di comunicazione tra processi, verranno poi scelte per essere utilizzate a seconda delle esigenze dell’applicazione.

comunicazione tra thread

Le code di messaggi sono una modalità di implementazione della concorrenza che consente ai thread di comunicare asincronamente attraverso una coda condivisa. In una coda di messaggi, i processi possono inviare messaggi alla coda e altri processi possono ricevere messaggi dalla coda. Ciò consente una comunicazione flessibile tra i processi senza la necessità di sincronizzazione esplicita.

In Python, la libreria queue fornisce un’implementazione delle code di messaggi. Ecco un esempio di come utilizzare una coda di messaggi in Python:

import queue
import threading
# Creiamo una coda di messaggi condivisa
q = queue.Queue()
def producer():
# Inviamo alcuni messaggi alla coda
for i in range(5):
q.put(i)
print(f"Inviato {i}")
def consumer():
while True:
# Riceviamo un messaggio dalla coda e lo stampiamo
item = q.get()
print(f"Ricevuto {item}")
q.task_done()
# Creiamo due thread, uno per il produttore e uno per il consumatore
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
# Avviamo i thread
t1.start()
t2.start()
# Attendiamo che il produttore finisca di inviare messaggi
t1.join()
# Interrompiamo il consumatore
q.put(None)

In questo esempio, il thread producer invia cinque messaggi alla coda condivisa q. Il thread consumer riceve i messaggi dalla coda e li stampa. Quando il produttore ha finito di inviare messaggi, il thread principale attende che il produttore finisca (t1.join()) e quindi interrompe il consumatore inviando un messaggio None alla coda.

Le code di messaggi sono utili in situazioni in cui i processi o i thread devono comunicare asincronamente o se si prevede una concorrenza interna alla coda. Tuttavia, possono essere più complesse da implementare rispetto ad altre modalità di implementazione della concorrenza come i thread o i processi.

Scelta del meccanismo di comunicazione tra processi

Ecco uno schema generale dei criteri di scelta delle metodologie di comunicazione tra processi:

  1. Tipo di informazione da scambiare: alcune informazioni possono essere scambiate più facilmente attraverso una modalità di comunicazione rispetto ad altre. Si pensi ad un testo piuttosto che una immagine.
  2. Numero di processi coinvolti: alcune modalità di comunicazione sono più adatte per un numero maggiore o minore di processi coinvolti.
  3. Necessità di sincronizzazione: alcune modalità di comunicazione richiedono una sincronizzazione più stretta tra i processi rispetto ad altre.
  4. Velocità di trasmissione: alcune modalità di comunicazione sono più veloci di altre per la trasmissione delle informazioni.
MetodologiaTipo di informazioneNumero di processiNecessità di sincronizzazioneVelocità di trasmissione
Coda di messaggi (FIFO)sempliceAltoBassaAlta
Variabili condiviseMediaBassoAltaBassa
SocketComplessaBassoBassaAlta

Ecco un algoritmo in pseudocodice che guidi nella scelta della metodologia di comunicazione tra processi:

Funzione scegli_metodologia_comunicazione(tipo_informazione, numero_processi, necessita_sincronizzazione, velocita_transmissione):
se tipo_informazione = semplice allora
return coda_messaggi
altrimenti se numero_processi = alto allora
return variabili_condivise
altrimenti se necessita_sincronizzazione = alta allora
return socket
altrimenti
return coda_messaggi
Fine funzione

Nota che questo algoritmo è solo un esempio di come si potrebbe scegliere la metodologia di comunicazione tra processi in base ai criteri elencati. Potrebbero esserci altri fattori da considerare a seconda delle esigenze dell’applicazione.

    Back to Blog

    Related Posts

    View All Posts »
    TPSIT: Proposta per un percorso moderno

    TPSIT: Proposta per un percorso moderno

    Da quando sono docente ho sempre insegnato la materia di TPSIT, Tecnologie e Progettazione di Sistemi Informatici e di telecomunicazioni. Ho sempre cercato di proporre un approccio moderno, cercando di coinvolgere gli studenti in progetti reali, con l'obiettivo di farli appassionare a questa materia, che è molto importante per il loro futuro lavorativo. In questo articolo propongo un percorso moderno, che ho sperimentato con successo, e che può essere utilizzato da altri docenti per proporre un percorso simile ai loro studenti.

    Qual è la differenza tra processo e thread?

    Qual è la differenza tra processo e thread?

    un processo è un'istanza di un programma in esecuzione con il proprio spazio di indirizzamento e risorse, mentre un thread è un sottoprocesso all'interno di un processo principale che condivide lo stesso spazio di indirizzamento e le risorse con il processo principale.

    le user story

    le user story

    capiamo in modo facile l'utilità e le caratteristiche delle user story