Concorrenza in Python - Multiprocessing

In questo capitolo ci concentreremo maggiormente sul confronto tra multiprocessing e multithreading.

Multiprocessing

È l'uso di due o più unità CPU all'interno di un singolo sistema di computer. È l'approccio migliore per ottenere il pieno potenziale dal nostro hardware utilizzando il numero completo di core CPU disponibili nel nostro sistema informatico.

Multithreading

È la capacità di una CPU di gestire l'uso del sistema operativo eseguendo più thread contemporaneamente. L'idea principale del multithreading è ottenere il parallelismo dividendo un processo in più thread.

La tabella seguente mostra alcune delle differenze importanti tra loro:

Multiprocessing Multiprogrammazione
Il multiprocessing si riferisce all'elaborazione di più processi contemporaneamente da più CPU. La multiprogrammazione mantiene più programmi contemporaneamente nella memoria principale e li esegue contemporaneamente utilizzando un'unica CPU.
Utilizza più CPU. Utilizza una singola CPU.
Consente l'elaborazione parallela. Ha luogo il cambio di contesto.
Meno tempo impiegato per elaborare i lavori. Più tempo impiegato per elaborare i lavori.
Facilita un utilizzo molto efficiente dei dispositivi del sistema informatico. Meno efficiente del multiprocessing.
Di solito più costoso. Tali sistemi sono meno costosi.

Eliminazione dell'impatto del blocco interprete globale (GIL)

Mentre si lavora con applicazioni simultanee, in Python è presente una limitazione chiamata GIL (Global Interpreter Lock). GIL non ci consente mai di utilizzare più core della CPU e quindi possiamo dire che non ci sono veri thread in Python. GIL è il mutex - blocco di mutua esclusione, che rende le cose thread-safe. In altre parole, possiamo dire che GIL impedisce a più thread di eseguire codice Python in parallelo. Il blocco può essere mantenuto da un solo thread alla volta e se si desidera eseguire un thread, è necessario prima acquisire il blocco.

Con l'uso del multiprocessing, possiamo bypassare efficacemente la limitazione causata da GIL -

  • Utilizzando il multiprocessing, stiamo utilizzando la capacità di più processi e quindi stiamo utilizzando più istanze del GIL.

  • Per questo motivo, non vi è alcuna restrizione all'esecuzione del bytecode di un thread all'interno dei nostri programmi in qualsiasi momento.

Avvio di processi in Python

I seguenti tre metodi possono essere utilizzati per avviare un processo in Python all'interno del modulo multiprocessing:

  • Fork
  • Spawn
  • Forkserver

Creare un processo con Fork

Il comando Fork è un comando standard trovato in UNIX. Viene utilizzato per creare nuovi processi chiamati processi figlio. Questo processo figlio viene eseguito contemporaneamente al processo chiamato processo padre. Questi processi figlio sono anche identici ai loro processi padre ed ereditano tutte le risorse disponibili per il genitore. Le seguenti chiamate di sistema vengono utilizzate durante la creazione di un processo con Fork:

  • fork()- È una chiamata di sistema generalmente implementata nel kernel. Viene utilizzato per creare una copia del processo. P>

  • getpid() - Questa chiamata di sistema restituisce l'ID di processo (PID) del processo chiamante.

Esempio

Il seguente esempio di script Python ti aiuterà a capire come creare un nuovo processo figlio e ottenere i PID dei processi figlio e genitore -

import os

def child():
   n = os.fork()
   
   if n > 0:
      print("PID of Parent process is : ", os.getpid())

   else:
      print("PID of Child process is : ", os.getpid())
child()

Produzione

PID of Parent process is : 25989
PID of Child process is : 25990

Creare un processo con Spawn

Spawn significa iniziare qualcosa di nuovo. Quindi, generare un processo significa la creazione di un nuovo processo da parte di un processo genitore. Il processo padre continua la sua esecuzione in modo asincrono o attende che il processo figlio termini la sua esecuzione. Segui questi passaggi per generare un processo:

  • Importazione del modulo multiprocessing.

  • Creazione del processo oggetto.

  • Avvio dell'attività di processo chiamando start() metodo.

  • Aspettando che il processo abbia terminato il suo lavoro ed esca chiamando join() metodo.

Esempio

Il seguente esempio di script Python aiuta a generare tre processi

import multiprocessing

def spawn_process(i):
   print ('This is process: %s' %i)
   return

if __name__ == '__main__':
   Process_jobs = []
   for i in range(3):
   p = multiprocessing.Process(target = spawn_process, args = (i,))
      Process_jobs.append(p)
   p.start()
   p.join()

Produzione

This is process: 0
This is process: 1
This is process: 2

Creazione di un processo con Forkserver

Il meccanismo forkserver è disponibile solo su quelle piattaforme UNIX selezionate che supportano il passaggio dei descrittori di file su Unix Pipe. Considera i seguenti punti per comprendere il funzionamento del meccanismo Forkserver:

  • Viene creata un'istanza di un server utilizzando il meccanismo Forkserver per l'avvio di un nuovo processo.

  • Il server riceve quindi il comando e gestisce tutte le richieste per la creazione di nuovi processi.

  • Per creare un nuovo processo, il nostro programma python invierà una richiesta a Forkserver e creerà un processo per noi.

  • Finalmente possiamo usare questo nuovo processo creato nei nostri programmi.

Processi daemon in Python

Pitone multiprocessingmodule ci permette di avere processi daemon attraverso la sua opzione daemonic. I processi daemon oi processi in esecuzione in background seguono un concetto simile a quello dei thread daemon. Per eseguire il processo in background, dobbiamo impostare il flag daemonic su true. Il processo daemon continuerà a essere eseguito fintanto che il processo principale è in esecuzione e terminerà dopo aver terminato la sua esecuzione o quando il programma principale verrebbe interrotto.

Esempio

Qui, stiamo usando lo stesso esempio usato nei thread del demone. L'unica differenza è il cambio di modulo damultithreading per multiprocessinge impostando la bandiera demoniaca su true. Tuttavia, ci sarebbe un cambiamento nell'output come mostrato di seguito:

import multiprocessing
import time

def nondaemonProcess():
   print("starting my Process")
   time.sleep(8)
   print("ending my Process")
def daemonProcess():
   while True:
   print("Hello")
   time.sleep(2)
if __name__ == '__main__':
   nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
   daemonProcess = multiprocessing.Process(target = daemonProcess)
   daemonProcess.daemon = True
   nondaemonProcess.daemon = False
   daemonProcess.start()
   nondaemonProcess.start()

Produzione

starting my Process
ending my Process

L'output è diverso rispetto a quello generato dai thread del daemon, perché il processo in modalità no daemon ha un output. Quindi, il processo demonico termina automaticamente dopo la fine dei programmi principali per evitare la persistenza dei processi in esecuzione.

Terminare i processi in Python

Possiamo uccidere o terminare immediatamente un processo utilizzando il file terminate()metodo. Useremo questo metodo per terminare il processo figlio, che è stato creato con l'aiuto della funzione, immediatamente prima di completare la sua esecuzione.

Esempio

import multiprocessing
import time
def Child_process():
   print ('Starting function')
   time.sleep(5)
   print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")

Produzione

My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated

L'output mostra che il programma termina prima dell'esecuzione del processo figlio che è stato creato con l'aiuto della funzione Child_process (). Ciò implica che il processo figlio è stato terminato correttamente.

Identificazione del processo corrente in Python

Ogni processo nel sistema operativo ha un'identità di processo nota come PID. In Python, possiamo scoprire il PID del processo corrente con l'aiuto del seguente comando:

import multiprocessing
print(multiprocessing.current_process().pid)

Esempio

Il seguente esempio di script Python aiuta a scoprire il PID del processo principale e il PID del processo figlio -

import multiprocessing
import time
def Child_process():
   print("PID of Child Process is: {}".format(multiprocessing.current_process().pid))
print("PID of Main process is: {}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()

Produzione

PID of Main process is: 9401
PID of Child Process is: 9402

Utilizzando un processo in una sottoclasse

Possiamo creare thread sottoclassando il file threading.Threadclasse. Inoltre, possiamo anche creare processi sottoclassando i filemultiprocessing.Processclasse. Per utilizzare un processo in una sottoclasse, dobbiamo considerare i seguenti punti:

  • Dobbiamo definire una nuova sottoclasse di Process classe.

  • Dobbiamo sovrascrivere il file _init_(self [,args] ) classe.

  • Dobbiamo sovrascrivere il di run(self [,args] ) metodo per implementare cosa Process

  • Dobbiamo avviare il processo invocando il filestart() metodo.

Esempio

import multiprocessing
class MyProcess(multiprocessing.Process):
   def run(self):
   print ('called run method in process: %s' %self.name)
   return
if __name__ == '__main__':
   jobs = []
   for i in range(5):
   P = MyProcess()
   jobs.append(P)
   P.start()
   P.join()

Produzione

called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5

Modulo multiprocessing Python - Classe Pool

Se parliamo di parallelo semplice processingtask nelle nostre applicazioni Python, quindi il modulo multiprocessing ci fornisce la classe Pool. I seguenti metodi diPool class può essere utilizzato per aumentare il numero di processi figli all'interno del nostro programma principale

metodo apply ()

Questo metodo è simile al.submit()metodo di .ThreadPoolExecutor.Si blocca finché il risultato non è pronto.

metodo apply_async ()

Quando abbiamo bisogno di un'esecuzione parallela dei nostri compiti, dobbiamo usare ilapply_async()metodo per inviare attività al pool. È un'operazione asincrona che non bloccherà il thread principale fino a quando non verranno eseguiti tutti i processi figlio.

metodo map ()

Proprio come il apply()metodo, si blocca anche fino a quando il risultato è pronto. È equivalente al built-inmap() funzione che divide i dati iterabili in un numero di blocchi e li invia al pool di processi come attività separate.

metodo map_async ()

È una variante del map() metodo come apply_async() è per il apply()metodo. Restituisce un oggetto risultato. Quando il risultato è pronto, gli viene applicato un callable. Il richiamabile deve essere completato immediatamente; in caso contrario, il thread che gestisce i risultati verrà bloccato.

Esempio

Il seguente esempio ti aiuterà a implementare un pool di processi per eseguire l'esecuzione parallela. Un semplice calcolo del quadrato del numero è stato eseguito applicando ilsquare() funzione tramite multiprocessing.Poolmetodo. Poipool.map() è stato utilizzato per inviare il 5, perché l'input è un elenco di numeri interi da 0 a 4. Il risultato verrebbe memorizzato in p_outputs ed è stampato.

def square(n):
   result = n*n
   return result
if __name__ == '__main__':
   inputs = list(range(5))
   p = multiprocessing.Pool(processes = 4)
   p_outputs = pool.map(function_square, inputs)
   p.close()
   p.join()
   print ('Pool :', p_outputs)

Produzione

Pool : [0, 1, 4, 9, 16]