🖥️ Programma vs Processo
Un programma è un’entità statica (codice oggetto salvato in file), mentre un processo è l’entità dinamica utilizzata dal sistema operativo per rappresentare una specifica esecuzione di un programma.
Il processo è caratterizzato dal codice eseguibile, dalla memoria indirizzata, dai thread, e dai descrittori delle risorse in uso.
🧵 Cos’è un Thread?
Un Thread è un flusso separato di esecuzione che racchiude una parte del codice da eseguire in modo indipendente dal resto del programma.
I thread condividono memoria e stato del processo (codice e variabili globali), ma hanno anche variabili locali accessibili solo al singolo thread.
📦 Modulo threading in Python
Importazione e creazione
import threading # Creare un thread t = threading.Thread(target=nome_funzione) # Avviare il thread t.start()
Esempio base con due thread
import threading
import time
def funzione(num):
print(f"Partenza del Thread {num}")
print("Elaboro.......\n")
time.sleep(2)
print(f"Finito lavoro {num}")
def main():
t1 = threading.Thread(target=funzione, args=("Primo",))
t2 = threading.Thread(target=funzione, args=("Secondo",))
t1.start()
t2.start()
print("Fine chiamata main\n")
if __name__ == "__main__":
main()
Elaboro…….
Partenza del Thread Secondo
Elaboro…….
Fine chiamata main
Finito lavoro Primo
Finito lavoro Secondo
🏷️ Identificazione dei Thread
Thread senza nome
import threading
import time
def funzione():
print("Partenza del", threading.current_thread().name)
time.sleep(2)
print("Finito", threading.current_thread().name)
t1 = threading.Thread(target=funzione)
t2 = threading.Thread(target=funzione)
t1.start()
t2.start()
Partenza del Thread-2
Finito Thread-1
Finito Thread-2
Thread con nome
import threading
import time
def funzione():
print("Partenza del", threading.current_thread().name)
time.sleep(2)
print("Finito", threading.current_thread().name)
t1 = threading.Thread(target=funzione, name="Primo")
t2 = threading.Thread(target=funzione, name="Secondo")
t1.start()
t2.start()
Partenza del Secondo
Finito Primo
Finito Secondo
🔗 Sincronizzazione con join()
Invocando il metodo join() su un oggetto Thread, il programma principale aspetta che il thread associato termini prima di continuare.
import threading
import time
def funzione():
print(f"Partenza del "+threading.current_thread().name)
time.sleep(2)
print(f"Finito lavoro "+threading.current_thread().name)
def main():
t1 = threading.Thread(target=funzione, name="Primo")
t2 = threading.Thread(target=funzione, name="Secondo")
t1.start()
t2.start()
t2.join() # Aspetta che t2 finisca
print("Fine chiamata main\n")
if __name__ == "__main__":
main()
Partenza del Secondo
Finito lavoro Primo
Finito lavoro Secondo
Fine chiamata main
🔒 Sezione Critica con Lock
Un Lock garantisce che solo un thread alla volta possa accedere a una sezione critica del codice, prevenendo race condition.
import threading
import time
luc = threading.Lock()
def funzione():
luc.acquire() # Acquisisce il lock
print(f"Partenza del ", threading.current_thread().name)
time.sleep(2)
print(f"Finito lavoro ", threading.current_thread().name)
luc.release() # Rilascia il lock
def main():
t1 = threading.Thread(target=funzione, name="Primo")
t2 = threading.Thread(target=funzione, name="Secondo")
t1.start()
t2.start()
t2.join()
if __name__ == "__main__":
main()
Differenze Con/Senza Lock
✅ Quando usare il Lock
- Accesso a risorse condivise
- Mantenere l’ordine di esecuzione
- Prevenire race condition
- Proteggere variabili globali
❌ Quando evitare il Lock
- Thread con dati indipendenti
- Problemi di prestazioni
- Strutture dati già thread-safe
- Operazioni atomiche
🌐 Applicazione Client-Server con Thread
Server Multithread
import socket
import threading
print_lock = threading.Lock()
active_threads = []
def operazione(conn):
while True:
ricevi = conn.recv(4096).decode()
print_lock.acquire()
print(ricevi + " da ", threading.current_thread().name)
print_lock.release()
risp = input("Inserisci una risposta: ")
conn.sendall(risp.encode())
if ricevi == "exit":
conn.close()
print_lock.acquire()
active_threads.remove(threading.current_thread())
print_lock.release()
break
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 8080))
s.listen()
try:
while True:
conn, address = s.accept()
print_lock.acquire()
print(f"Connesso con {address}")
print_lock.release()
t = threading.Thread(target=operazione, args=(conn,))
t.start()
print_lock.acquire()
active_threads.append(t)
print_lock.release()
except KeyboardInterrupt:
print("\nServer in arresto...")
s.close()
for t in active_threads:
t.join()
print("Server arrestato")
if __name__ == "__main__":
main()
Client
import socket
def client():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 8080))
while True:
msg = input("Inserisci un messaggio: ")
s.sendall(msg.encode())
rice = s.recv(4096).decode()
print(rice)
if msg == "exit":
s.close()
break
def main():
client()
if __name__ == "__main__":
main()
🔢 Server per Calcolo Parallelo
Server
import socket
import threading
calculation_lock = threading.Lock()
active_threads = []
def handle_calculation(conn, num):
quadrato = num * num
cubo = num * num * num
response = f"Quadrato: {quadrato}, Cubo: {cubo}"
conn.sendall(response.encode())
conn.close()
calculation_lock.acquire()
active_threads.remove(threading.current_thread())
calculation_lock.release()
def start_server():
host = '127.0.0.1'
port = 12345
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen()
print(f"Server calcoli su {host}:{port}")
try:
while True:
conn, addr = server_socket.accept()
data = conn.recv(1024).decode()
num = int(data)
t = threading.Thread(target=handle_calculation, args=(conn, num))
t.start()
calculation_lock.acquire()
active_threads.append(t)
calculation_lock.release()
except KeyboardInterrupt:
print("\nAttesa completamento thread...")
calculation_lock.acquire()
for t in active_threads:
t.join()
calculation_lock.release()
print("Server arrestato")
server_socket.close()
if __name__ == "__main__":
start_server()
Client
import socket
def main():
host = '127.0.0.1'
port = 12345
while True:
num = input("Inserisci un numero (o 'exit'): ")
if num.lower() == 'exit':
break
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
client_socket.sendall(num.encode())
data = client_socket.recv(1024).decode()
print(f"Risultati: {data}")
client_socket.close()
if __name__ == "__main__":
main()
⚠️ Problemi di Concorrenza
1. Race Condition
Due thread modificano la stessa variabile contemporaneamente, perdendo alcuni incrementi.
import threading
contatore = 0
def incrementa():
global contatore
for _ in range(100000):
contatore += 1
t1 = threading.Thread(target=incrementa)
t2 = threading.Thread(target=incrementa)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Valore: {contatore}") # Dovrebbe essere 200000
2. Deadlock
Due thread si bloccano a vicenda aspettando lock che l’altro possiede.
import threading
import time
lockA = threading.Lock()
lockB = threading.Lock()
def thread1():
lockA.acquire()
time.sleep(1)
lockB.acquire() # Bloccato!
lockB.release()
lockA.release()
def thread2():
lockB.acquire()
time.sleep(1)
lockA.acquire() # Bloccato!
lockA.release()
lockB.release()
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
3. Output Mescolato
Senza lock, le stampe dei thread si mescolano in modo imprevedibile.
import threading
def stampa(msg):
print(f"Inizio: {msg}")
print(f"Fine: {msg}")
t1 = threading.Thread(target=stampa, args=("A",))
t2 = threading.Thread(target=stampa, args=("B",))
t1.start()
t2.start()
Inizio: B
Fine: A
Fine: B
📋 Pattern di Programmazione
🔷 Thread per Connessione
Ogni client viene gestito da un thread separato per consentire connessioni multiple simultanee.
🔷 Lock Condiviso
Protegge le variabili condivise tra thread per evitare race condition e mantenere la consistenza dei dati.
🔷 Chiusura Sicura
Le connessioni e le risorse vengono chiuse correttamente per evitare memory leak e porte bloccate.
🔷 Simple Protocol
Protocollo di comunicazione semplice basato su stringhe per facilitare il debug e la manutenzione.
🧪 Come Testare gli Esempi
Avviare il server in un terminale: python nome_server.py
Avviare uno o più client in terminali separati: python nome_client.py
Per testare il multithreading, apri più client contemporaneamente e osserva come il server gestisce le connessioni in parallelo
🚀 Altre Lezioni
Continua il tuo percorso sui socket in Python
📚 Percorso Formativo
Completa il tuo percorso sui socket in Python