Simultaneidade em Python - Multiprocessamento

Neste capítulo, vamos nos concentrar mais na comparação entre multiprocessamento e multithreading.

Multiprocessamento

É o uso de duas ou mais unidades de CPU em um único sistema de computador. É a melhor abordagem para obter todo o potencial de nosso hardware, utilizando o número total de núcleos de CPU disponíveis em nosso sistema de computador.

Multithreading

É a capacidade de uma CPU de gerenciar o uso do sistema operacional executando vários threads simultaneamente. A ideia principal do multithreading é atingir o paralelismo dividindo um processo em vários threads.

A tabela a seguir mostra algumas das diferenças importantes entre eles -

Multiprocessamento Multiprogramação
Multiprocessamento refere-se ao processamento de vários processos ao mesmo tempo por várias CPUs. A multiprogramação mantém vários programas na memória principal ao mesmo tempo e os executa simultaneamente utilizando uma única CPU.
Ele utiliza várias CPUs. Ele utiliza uma única CPU.
Ele permite o processamento paralelo. A comutação de contexto ocorre.
Menos tempo para processar os trabalhos. Mais tempo para processar os trabalhos.
Facilita a utilização muito eficiente de dispositivos do sistema de computador. Menos eficiente do que o multiprocessamento.
Geralmente mais caro. Esses sistemas são mais baratos.

Eliminando o impacto do bloqueio global do intérprete (GIL)

Ao trabalhar com aplicativos simultâneos, há uma limitação presente no Python chamada de GIL (Global Interpreter Lock). GIL nunca nos permite utilizar múltiplos núcleos de CPU e, portanto, podemos dizer que não há threads verdadeiros em Python. GIL é o mutex - bloqueio de exclusão mútua, que torna as coisas thread-safe. Em outras palavras, podemos dizer que GIL evita que vários threads executem código Python em paralelo. O bloqueio pode ser mantido por apenas um encadeamento por vez e, se quisermos executar um encadeamento, ele deve primeiro adquirir o bloqueio.

Com o uso de multiprocessamento, podemos efetivamente contornar a limitação causada pelo GIL -

  • Ao usar multiprocessamento, estamos utilizando a capacidade de vários processos e, portanto, estamos utilizando várias instâncias do GIL.

  • Devido a isso, não há restrição de executar o bytecode de um thread em nossos programas a qualquer momento.

Iniciando processos em Python

Os três métodos a seguir podem ser usados ​​para iniciar um processo em Python dentro do módulo de multiprocessamento -

  • Fork
  • Spawn
  • Forkserver

Criando um processo com Fork

O comando Fork é um comando padrão encontrado no UNIX. É usado para criar novos processos chamados processos filhos. Esse processo filho é executado simultaneamente com o processo denominado processo pai. Esses processos filho também são idênticos aos processos pai e herdam todos os recursos disponíveis para o pai. As seguintes chamadas de sistema são usadas durante a criação de um processo com Fork -

  • fork()- É uma chamada de sistema geralmente implementada no kernel. É usado para criar uma cópia do process.p>

  • getpid() - Esta chamada de sistema retorna o ID do processo (PID) do processo de chamada.

Exemplo

O seguinte exemplo de script Python irá ajudá-lo a compreender como criar um novo processo filho e obter os PIDs dos processos filho e pai -

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

Resultado

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

Criando um processo com Spawn

Spawn significa começar algo novo. Portanto, gerar um processo significa a criação de um novo processo por um processo pai. O processo pai continua sua execução de forma assíncrona ou espera até que o processo filho termine sua execução. Siga estas etapas para gerar um processo -

  • Importando módulo de multiprocessamento.

  • Criando o processo de objeto.

  • Iniciando a atividade do processo chamando start() método.

  • Esperar até que o processo termine seu trabalho e sair chamando join() método.

Exemplo

O exemplo a seguir de script Python ajuda na geração de três processos

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

Resultado

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

Criando um processo com o Forkserver

O mecanismo Forkserver está disponível apenas nas plataformas UNIX selecionadas que suportam a passagem de descritores de arquivo por Pipes Unix. Considere os seguintes pontos para entender o funcionamento do mecanismo Forkserver -

  • Um servidor é instanciado usando o mecanismo Forkserver para iniciar um novo processo.

  • O servidor então recebe o comando e trata de todas as solicitações de criação de novos processos.

  • Para criar um novo processo, nosso programa python enviará uma solicitação ao Forkserver e criará um processo para nós.

  • Por fim, podemos usar este novo processo criado em nossos programas.

Processos daemon em Python

Pitão multiprocessingmódulo nos permite ter processos daemon através de sua opção daemônica. Os processos daemon ou os processos que estão sendo executados em segundo plano seguem um conceito semelhante aos encadeamentos daemon. Para executar o processo em segundo plano, precisamos definir o sinalizador daemonic como verdadeiro. O processo daemon continuará a ser executado enquanto o processo principal estiver em execução e será encerrado após o término de sua execução ou quando o programa principal for encerrado.

Exemplo

Aqui, estamos usando o mesmo exemplo usado nas threads daemon. A única diferença é a mudança de módulo demultithreading para multiprocessinge definindo o sinalizador demoníaco como verdadeiro. No entanto, haveria uma mudança na produção conforme mostrado abaixo -

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

Resultado

starting my Process
ending my Process

A saída é diferente quando comparada àquela gerada por encadeamentos daemon, porque o processo em nenhum modo daemon tem uma saída. Portanto, o processo daemônico termina automaticamente após o término dos programas principais para evitar a persistência dos processos em execução.

Encerrando processos em Python

Podemos matar ou encerrar um processo imediatamente usando o terminate()método. Usaremos este método para encerrar o processo filho, que foi criado com a ajuda da função, imediatamente antes de concluir sua execução.

Exemplo

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

Resultado

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

A saída mostra que o programa termina antes da execução do processo filho que foi criado com a ajuda da função Child_process (). Isso implica que o processo filho foi encerrado com sucesso.

Identificar o processo atual em Python

Cada processo no sistema operacional tem uma identidade de processo conhecida como PID. Em Python, podemos descobrir o PID do processo atual com a ajuda do seguinte comando -

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

Exemplo

O exemplo a seguir de script Python ajuda a descobrir o PID do processo principal, bem como o PID do processo filho -

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

Resultado

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

Usando um processo na subclasse

Podemos criar tópicos subclassificando o threading.Threadclasse. Além disso, também podemos criar processos subclassificando omultiprocessing.Processclasse. Para usar um processo na subclasse, precisamos considerar os seguintes pontos -

  • Precisamos definir uma nova subclasse do Process classe.

  • Precisamos substituir o _init_(self [,args] ) classe.

  • Precisamos substituir o do run(self [,args] ) método para implementar o que Process

  • Precisamos iniciar o processo invocando ostart() método.

Exemplo

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

Resultado

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

Módulo de multiprocessamento Python - classe Pool

Se falarmos sobre paralelo simples processingtarefas em nossos aplicativos Python, então o módulo de multiprocessamento nos fornece a classe Pool. Os seguintes métodos dePool classe pode ser usada para aumentar o número de processos filho dentro do nosso programa principal

método apply ()

Este método é semelhante ao.submit()método de .ThreadPoolExecutor.Bloqueia até que o resultado esteja pronto.

método apply_async ()

Quando precisamos da execução paralela de nossas tarefas, precisamos usar oapply_async()método para enviar tarefas ao pool. É uma operação assíncrona que não bloqueará o thread principal até que todos os processos filho sejam executados.

método map ()

Assim como o apply()método, ele também bloqueia até que o resultado esteja pronto. É equivalente ao embutidomap() função que divide os dados iteráveis ​​em vários blocos e os envia ao conjunto de processos como tarefas separadas.

método map_async ()

É uma variante do map() método como apply_async() é para o apply()método. Ele retorna um objeto de resultado. Quando o resultado fica pronto, um chamável é aplicado a ele. O chamável deve ser concluído imediatamente; caso contrário, o thread que lida com os resultados será bloqueado.

Exemplo

O exemplo a seguir o ajudará a implementar um pool de processos para realizar a execução paralela. Um cálculo simples do quadrado do número foi realizado aplicando osquare() função através do multiprocessing.Poolmétodo. Entãopool.map() foi usado para enviar o 5, porque a entrada é uma lista de inteiros de 0 a 4. O resultado seria armazenado em p_outputs e é impresso.

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)

Resultado

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