Współbieżność w Pythonie - wieloprocesorowość

W tym rozdziale skupimy się bardziej na porównaniu między przetwarzaniem wieloprocesowym a wielowątkowością.

Wieloprocesowość

Jest to użycie dwóch lub więcej jednostek CPU w jednym systemie komputerowym. Jest to najlepsze podejście do pełnego wykorzystania potencjału naszego sprzętu poprzez wykorzystanie pełnej liczby rdzeni procesora dostępnych w naszym systemie komputerowym.

Wielowątkowość

Jest to zdolność procesora do zarządzania wykorzystaniem systemu operacyjnego poprzez jednoczesne wykonywanie wielu wątków. Główną ideą wielowątkowości jest osiągnięcie równoległości poprzez podzielenie procesu na wiele wątków.

Poniższa tabela przedstawia niektóre z ważnych różnic między nimi -

Wieloprocesowość Wieloprogramowanie
Wieloprocesorowość oznacza przetwarzanie wielu procesów w tym samym czasie przez wiele procesorów. Wieloprogramowanie zachowuje kilka programów w pamięci głównej w tym samym czasie i wykonuje je jednocześnie, wykorzystując jeden procesor.
Wykorzystuje wiele procesorów. Wykorzystuje pojedynczy procesor.
Umożliwia równoległe przetwarzanie. Następuje przełączanie kontekstów.
Mniej czasu zajmuje przetwarzanie zadań. Więcej czasu zajmuje przetwarzanie zadań.
Ułatwia bardzo efektywne wykorzystanie urządzeń systemu komputerowego. Mniej wydajne niż przetwarzanie wieloprocesowe.
Zwykle droższe. Takie systemy są tańsze.

Eliminacja wpływu globalnej blokady tłumacza (GIL)

Podczas pracy z aplikacjami współbieżnymi w Pythonie występuje ograniczenie o nazwie GIL (Global Interpreter Lock). GIL nigdy nie pozwala nam na wykorzystanie wielu rdzeni procesora, dlatego możemy powiedzieć, że w Pythonie nie ma prawdziwych wątków. GIL to mutex - blokada wzajemnego wykluczania, która sprawia, że ​​rzeczy są bezpieczne. Innymi słowy, możemy powiedzieć, że GIL zapobiega równoległemu wykonywaniu kodu Pythona przez wiele wątków. Blokada może być utrzymywana tylko przez jeden wątek naraz, a jeśli chcemy wykonać wątek, musi najpierw uzyskać blokadę.

Wykorzystując wieloprocesorowość możemy skutecznie ominąć ograniczenie spowodowane przez GIL -

  • Korzystając z przetwarzania wieloprocesowego, wykorzystujemy możliwości wielu procesów, a zatem wykorzystujemy wiele instancji GIL.

  • Z tego powodu nie ma ograniczeń co do wykonywania kodu bajtowego jednego wątku w naszych programach w dowolnym momencie.

Uruchamianie procesów w Pythonie

Do uruchomienia procesu w Pythonie w module wieloprocesorowym można użyć następujących trzech metod -

  • Fork
  • Spawn
  • Forkserver

Tworzenie procesu za pomocą Fork

Polecenie rozwidlenia jest standardowym poleceniem występującym w systemie UNIX. Służy do tworzenia nowych procesów zwanych procesami potomnymi. Ten proces potomny działa równolegle z procesem nazywanym procesem nadrzędnym. Te procesy potomne są również identyczne z ich procesami nadrzędnymi i dziedziczą wszystkie zasoby dostępne dla elementu nadrzędnego. Podczas tworzenia procesu za pomocą Fork używane są następujące wywołania systemowe -

  • fork()- Jest to wywołanie systemowe generalnie zaimplementowane w jądrze. Służy do tworzenia kopii procesu. P>

  • getpid() - To wywołanie systemowe zwraca identyfikator procesu (PID) procesu wywołującego.

Przykład

Poniższy przykład skryptu w Pythonie pomoże ci zrozumieć, jak utworzyć nowy proces potomny i uzyskać identyfikatory PID procesów potomnych i nadrzędnych -

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()

Wynik

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

Tworzenie procesu za pomocą Spawn

Spawn oznacza rozpoczęcie czegoś nowego. Stąd tworzenie procesu oznacza utworzenie nowego procesu przez proces macierzysty. Proces nadrzędny kontynuuje wykonywanie asynchronicznie lub czeka, aż proces potomny zakończy wykonywanie. Wykonaj następujące kroki, aby utworzyć proces -

  • Importowanie modułu wieloprocesorowego.

  • Tworzenie procesu obiektowego.

  • Rozpoczęcie czynności procesu przez wywołanie start() metoda.

  • Czekam, aż proces zakończy swoją pracę i zakończ, dzwoniąc join() metoda.

Przykład

Poniższy przykład skryptu w Pythonie pomaga w uruchomieniu trzech procesów

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()

Wynik

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

Tworzenie procesu za pomocą Forkserver

Mechanizm Forkserver jest dostępny tylko na wybranych platformach UNIX, które obsługują przekazywanie deskryptorów plików przez potoki Unix. Rozważ następujące punkty, aby zrozumieć działanie mechanizmu Forkserver -

  • Serwer jest tworzony przy użyciu mechanizmu Forkserver do uruchamiania nowego procesu.

  • Następnie serwer otrzymuje polecenie i obsługuje wszystkie żądania tworzenia nowych procesów.

  • Aby utworzyć nowy proces, nasz program w Pythonie wyśle ​​żądanie do Forkserver i stworzy dla nas proces.

  • Nareszcie możemy wykorzystać ten nowo utworzony proces w naszych programach.

Procesy demona w Pythonie

Pyton multiprocessingmoduł pozwala nam mieć procesy demonów poprzez jego opcję demoniczną. Procesy demona lub procesy działające w tle działają podobnie jak wątki demona. Aby wykonać proces w tle, musimy ustawić flagę demona na true. Proces demona będzie działał tak długo, jak długo będzie wykonywany proces główny i zakończy się po zakończeniu jego wykonywania lub gdy główny program zostanie zabity.

Przykład

Tutaj używamy tego samego przykładu, który jest używany w wątkach demonów. Jedyną różnicą jest zmiana modułu zmultithreading do multiprocessingi ustawiając flagę demona na true. Jednak nastąpiłaby zmiana w produkcji, jak pokazano poniżej -

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()

Wynik

starting my Process
ending my Process

Dane wyjściowe są inne niż te generowane przez wątki demona, ponieważ proces w trybie bez demona ma dane wyjściowe. W związku z tym proces demoniczny kończy się automatycznie po zakończeniu działania głównych programów, aby uniknąć trwałości uruchomionych procesów.

Zakończenie procesów w Pythonie

Możemy natychmiast zabić lub zakończyć proces przy użyciu rozszerzenia terminate()metoda. Użyjemy tej metody do zakończenia procesu potomnego, który został utworzony za pomocą funkcji, bezpośrednio przed zakończeniem jego wykonania.

Przykład

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")

Wynik

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

Wynik pokazuje, że program kończy działanie przed wykonaniem procesu potomnego, który został utworzony za pomocą funkcji Child_process (). Oznacza to, że proces potomny został pomyślnie zakończony.

Identyfikowanie bieżącego procesu w Pythonie

Każdy proces w systemie operacyjnym ma tożsamość znaną jako PID. W Pythonie możemy znaleźć PID bieżącego procesu za pomocą następującego polecenia -

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

Przykład

Poniższy przykład skryptu w Pythonie pomaga znaleźć PID procesu głównego, a także PID procesu potomnego -

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()

Wynik

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

Korzystanie z procesu w podklasie

Możemy tworzyć wątki, dzieląc klasy threading.Threadklasa. Ponadto możemy również tworzyć procesy, klasyfikując plikimultiprocessing.Processklasa. Aby użyć procesu w podklasie, musimy wziąć pod uwagę następujące punkty -

  • Musimy zdefiniować nową podklasę klasy Process klasa.

  • Musimy zastąpić _init_(self [,args] ) klasa.

  • Musimy zastąpić run(self [,args] ) metoda wdrożenia czego Process

  • Musimy rozpocząć proces od wywołaniastart() metoda.

Przykład

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()

Wynik

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

Moduł wieloprocesorowy Pythona - klasa puli

Jeśli mówimy o prostej równoległości processingzadań w naszych aplikacjach Python, a następnie moduł wieloprocesorowy dostarcza nam klasę Pool. Następujące metodyPool class może służyć do uruchamiania wielu procesów potomnych w naszym głównym programie

apply (), metoda

Ta metoda jest podobna do.submit()metoda .ThreadPoolExecutor.Blokuje się, aż wynik będzie gotowy.

Apply_async (), metoda

Kiedy potrzebujemy równoległego wykonywania naszych zadań, musimy użyćapply_async()metoda przesyłania zadań do puli. Jest to operacja asynchroniczna, która nie zablokuje głównego wątku, dopóki nie zostaną wykonane wszystkie procesy potomne.

map () metoda

Podobnie jak apply()metoda, blokuje również, dopóki wynik nie będzie gotowy. Jest odpowiednikiem wbudowanegomap() funkcja, która dzieli iterowalne dane na kilka porcji i przesyła do puli procesów jako oddzielne zadania.

map_async (), metoda

Jest to wariant map() metoda jako apply_async() jest do apply()metoda. Zwraca obiekt wynikowy. Kiedy wynik stanie się gotowy, zostanie do niego zastosowane wywołanie. Wywołanie musi zostać zakończone natychmiast; w przeciwnym razie wątek obsługujący wyniki zostanie zablokowany.

Przykład

Poniższy przykład pomoże Ci zaimplementować pulę procesów do wykonywania równoległego wykonywania. Proste obliczenie kwadratu liczby zostało przeprowadzone przez zastosowaniesquare() funkcji za pośrednictwem multiprocessing.Poolmetoda. Następniepool.map() został użyty do przesłania 5, ponieważ dane wejściowe to lista liczb całkowitych od 0 do 4. Wynik zostanie zapisany w p_outputs i jest drukowany.

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)

Wynik

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