Introduzione
In questo articolo vedremo come implementare con il linguaggio Python, uno degli aspetti importanti e fondamentali nella programmazione su Linux: il Forking. Questa operazione è molto importante quando vogliamo gestire più processi contemporaneamente attraverso la programmazione ed è quindi un argomento importante da conoscere per quanto riguarda il Multiprocessing in Python.
Il Forking
Nel mondo della programmazione, si ha una fork quando un processo crea una copia perfetta di se stesso in memoria. Il processo chiamante viene generalmente chiamato processo padre (parent process), mentre quello copiato sarà il processo figlio (child process).
La copia (forking) viene effettuata in memoria, creando un nuovo spazio di indirizzamento, in cui tutti i segmenti di memoria vengono completamente copiati da parent process. Da quel momento in poi l’esecuzione dei processi parent e child diventano completamente indipendenti tra di loro, tanto da avere due diversi PID (Process IDentifier).
Fork in Python
In Python è possibile applicare il forking grazie alla funzione di sistema chiamata fork() appartenente al modulo os.
Quando un processo scritto in Python invoca la funzione fork(), questa crea una copia del processo. Questa copia (child process) ottiene tutti i dati ed il codice direttamente dal processo parent, per poi essere eseguita come un processo completamente indipendente ottenendo un proprio PID dal sistema operativo.
Comunque, la prima cosa di cui bisogna tenere conto quando utilizziamo la funzione fork() è che il programma si divide in due copie perfette, che eseguiranno le stesse operazioni. Generalmente questo non è certamente quello che vogliamo, anzi, vogliamo che ciascuno dei due processi esegui delle operazioni leggermente diverse. Per fare questo possiamo sfruttare il valore restituito dalla funzione fork().
L’esempio seguente ci mostra in modo molto semplice il funzionamento della funzione fork, ottenendo da terminale il pid dei processi parent e child.
import os print("PID parent {}".format(os.getpid())) pid = os.fork() print("PID child {}".format(pid))
Infatti grazie al valore restituito dalla funzione fork() è possibile sapere in quale processo ci stiamo trovando. Se il valore di ritorno è 0 allora vuol dire che stiamo nel processo child. Se invece è un valore positivo allora ci troviamo nel processo parent, il valore positivo ottenuto non è altro che il pid del child process. Infine, se il valore restituito è negativo allora vuol dire che qualcosa è andato storto e dobbiamo in qualche modo gestire l’errore.
In questo caso allora possiamo differenziare il comportamento del programma a seconda se ci troviamo nel processo child o parent e lo possiamo fare sfruttando appunto il valore restituito da fork().
import os pid = os.fork() if pid > 0: print("This is written by the parent process {}".format(os.getpid())) else: print("This is written by the child process {}".format(os.getpid()))
Da questo esempio possiamo vedere come possiamo dividere le operazioni da eseguire attraverso un costrutto IF-ELSE.
Esempio
Ma passiamo ad un esempio più complesso. Vediamo il caso in cui vogliamo far partire più processi child. In questo caso dobbiamo introdurre la funzione
os._exit(0)
questa viene chiamata all’interno del processo child per evitare che generi a sua volta altri processi child fino a finire in cicli incontrollati.
import os NUM_PROC = 5 for process in range(NUM_PROC): pid = os.fork() if pid > 0: print("This is the parent process {}".format(os.getpid())) else: print("This is the child process {}".format(os.getpid()))) os._exit(0) print("Parent process is closing")
Eseguendo il codice vedremo che il parent child genererà tutti e 5 i processi child desiderati e poi si chiuderà, nel frattempo i 5 processi child eseguiranno le loro operazioni e poi si chiuderanno a loro volta.
Ma come possiamo vedere il processo parent è il primo a chiudersi. Se invece volessimo fare in modo che il processo parent rimanga in attesa che tutti i suoi processi child eseguino il loro compito e si chiudano, attendendo il loro risultato e poi chiudersi per ultimo?
Esiste in Python una funzione apposita
os.waitpid(pid, 0)
che attende la chiusura di un processo (identificato con il pid) prima di passare alle istruzioni successive.
Intruduciamolo nel codice precedente
import os NUM_PROC = 5 children = [] for process in range(NUM_PROC): pid = os.fork() if pid > 0: print("This is the parent process {}".format(os.getpid())) children.append(pid) else: print("This is the child process {}".format(os.getpid()))) os._exit(0) for i, proc in enumerate(children): os.waitpid(proc, 0) print("Parent process is closing")
Questa volta abbiamo avuto bisogno di un array che contenesse tutti i valori dei pid dei processi child. Eseguendo il codice otterremo il risultato voluto.
Conclusioni
In questo articolo hai visto come poter realizzare e gestire il multiprocessing partendo da un solo codice, grazie alla tecnica del forking. Questa tecnica è una delle possibili tecniche utilizzate in Python per ottenere il MultiProcessing.
]