from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

from urllib.request import urlopen
from time import perf_counter

def work(n):    
    with urlopen("https://www.google.com/#{n}") as f:
        contents = f.read(32)
    return contents

def run_pool(pool_type):
    with pool_type() as pool:
        start = perf_counter()
        results = pool.map(work, numbers)
    print ("Time:", perf_counter()-start)
    print ((_ for _ in results))    

if __name__ == '__main__':
    numbers = (x for x in range(1,16))
    
    # Run the task using a thread pool
    run_pool(ThreadPoolExecutor)
    
    # Run the task using a process pool
    run_pool(ProcessPoolExecutor)

Como funciona o multiprocessamento Python

No exemplo acima, o concurrent.futures módulo fornece objetos de pool de alto nível para execução de trabalho em threads (ThreadPoolExecutor) e processos (ProcessPoolExecutor). Ambos os tipos de pool têm a mesma API, portanto você pode criar funções que funcionem de forma intercambiável com ambos, como mostra o exemplo.

Nós usamos run_pool para submeter instâncias do work função para os diferentes tipos de piscinas. Por padrão, cada instância do pool usa um único thread ou processo por núcleo de CPU disponível. Há uma certa sobrecarga associada à criação de pools, portanto, não exagere. Se você for processar muitos trabalhos durante um longo período de tempo, crie o pool primeiro e não descarte-o até terminar. Com o Executor objetos, você pode usar um gerenciador de contexto para criar e descartar pools (with/as).

pool.map() é a função que usamos para subdividir o trabalho. O pool.map() A função recebe uma função com uma lista de argumentos para aplicar a cada instância da função, divide o trabalho em partes (você pode especificar o tamanho da parte, mas o padrão geralmente é adequado) e alimenta cada parte para um thread ou processo de trabalho.