Por qué, cuándo y cómo usar multiprocesamiento y multiprocesamiento en Python

Saludo, Khabrovsk. En este momento, OTUS ha abierto un set para el curso de Machine Learning , en relación con esto hemos traducido para usted un "cuento de hadas" muy interesante. Vamos.




Érase una vez, en una galaxia distante, distante ... Un

mago sabio y poderoso vivía en un pequeño pueblo en medio del desierto. Y se llamaban Dumbledalf. No solo era sabio y poderoso, sino que también ayudaba a las personas que venían de tierras lejanas a pedir ayuda a un mago. Nuestra historia comenzó cuando un viajero le trajo al mago un pergamino mágico. El viajero no sabía lo que había en el pergamino, solo sabía que si alguien podía revelar todos los secretos del pergamino, entonces esto era Dumbledalf.

Capítulo 1: Rosca única, proceso único


Si aún no lo ha adivinado, hice una analogía con el procesador y sus funciones. Nuestro asistente es un procesador, y un desplazamiento es una lista de enlaces que conducen al poder y al conocimiento de Python para dominarlo.

El primer pensamiento del mago que descifró la lista sin ninguna dificultad fue enviar a su fiel amigo (Garrigorn? Lo sé, sé que eso suena horrible) a cada uno de los lugares que se describieron en el pergamino para encontrar y traer lo que pudo encontrar allí.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [3]:
%%time

results = []
for url in urls:
    with urllib.request.urlopen(url) as src:
        results.append(src)

CPU times: user 135 ms, sys: 283 µs, total: 135 ms
Wall time: 12.3 s
In [ ]:

Como puede ver, simplemente iteramos sobre las URL una por una usando el bucle for y leemos la respuesta. Gracias al %% de tiempo y la magia de IPython , podemos ver que me llevó unos 12 segundos con mi triste internet.

Capítulo 2: Multithreading


No sin razón, el mago era famoso por su sabiduría; rápidamente pudo encontrar una manera mucho más efectiva. En lugar de enviar a una persona a cada lugar en orden, ¡por qué no reunir un escuadrón de asociados confiables y enviarlos a diferentes partes del mundo al mismo tiempo! ¡El mago podrá unir todo el conocimiento que traen a la vez!

Así es, en lugar de ver la lista en un bucle en secuencia, podemos usar subprocesos múltiples para acceder a múltiples URL a la vez.

In [1]:
import urllib.request
from concurrent.futures import ThreadPoolExecutor
In [2]:
urls = [
  'http://www.python.org',
  'https://docs.python.org/3/',
  'https://docs.python.org/3/whatsnew/3.7.html',
  'https://docs.python.org/3/tutorial/index.html',
  'https://docs.python.org/3/library/index.html',
  'https://docs.python.org/3/reference/index.html',
  'https://docs.python.org/3/using/index.html',
  'https://docs.python.org/3/howto/index.html',
  'https://docs.python.org/3/installing/index.html',
  'https://docs.python.org/3/distributing/index.html',
  'https://docs.python.org/3/extending/index.html',
  'https://docs.python.org/3/c-api/index.html',
  'https://docs.python.org/3/faq/index.html'
  ]
In [4]:
%%time

with ThreadPoolExecutor(4) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 8.27 ms, total: 130 ms
Wall time: 3.83 s
In [5]:
%%time

with ThreadPoolExecutor(8) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 122 ms, sys: 14.7 ms, total: 137 ms
Wall time: 1.79 s
In [6]:
%%time

with ThreadPoolExecutor(16) as executor:
    results = executor.map(urllib.request.urlopen, urls)

CPU times: user 143 ms, sys: 3.88 ms, total: 147 ms
Wall time: 1.32 s
In [ ]:

¡Mucho mejor! Casi como ... magia. El uso de múltiples subprocesos puede acelerar significativamente muchas tareas de E / S. En mi caso, la mayor parte del tiempo dedicado a leer las URL se debe a la latencia de la red. Usted adivinó que los programas vinculados a E / S pasan la mayor parte de sus vidas esperando entrada o salida (al igual que un asistente espera a que sus amigos vayan a lugares desde el pergamino y regresen). Esto puede ser de entrada / salida desde la red, base de datos, archivo o desde el usuario. Dicha E / S generalmente lleva mucho tiempo, porque la fuente puede necesitar realizar un preprocesamiento antes de transferir los datos a la E / S. Por ejemplo, un procesador contará mucho más rápido que una conexión de red transmitirá datos (a una velocidad de aproximadamentecomo Flash contra tu abuela).

Nota : multithreadingpuede ser muy útil en tareas como limpiar páginas web.

Capítulo 3: Multiprocesamiento


Pasaron los años, la fama del buen mago creció, y con ella creció la envidia de un mago oscuro imparcial (¿Sarumort? ¿O tal vez Voldemand?). Armado con una astucia inconmensurable e impulsado por la envidia, el mago oscuro lanzó una terrible maldición sobre Dumbledalf. Cuando la maldición lo alcanzó, Dumbledalf se dio cuenta de que solo tenía unos momentos para alejarlo. Desesperado, rebuscó en sus libros de hechizos y rápidamente encontró un contra hechizo que se suponía que funcionaba. El único problema era que el mago necesitaba calcular la suma de todos los números primos de menos de 1,000,000. Un hechizo extraño, por supuesto, pero lo que tenemos.

El mago sabía que calcular el valor sería trivial si tuviera suficiente tiempo, pero no tenía ese lujo. A pesar del hecho de que es un gran mago, sin embargo, está limitado por su humanidad y puede verificar la simplicidad solo un número a la vez. Si decidiera simplemente sumar los números primos uno tras otro, tomaría demasiado tiempo. Cuando solo quedaban unos segundos antes de aplicar el contrahechizo, de repente recordó el hechizo de multiprocesamiento , que había aprendido del pergamino mágico hace muchos años. Este hechizo le permitirá copiarse a sí mismo para distribuir números entre sus copias y verificar varios al mismo tiempo. Y al final, todo lo que necesita hacer es simplemente sumar los números que él y sus copias descubrirán.

In [1]:
from multiprocessing import Pool
In [2]:
def if_prime(x):
    if x <= 1:
        return 0
    elif x <= 3:
        return x
    elif x % 2 == 0 or x % 3 == 0:
        return 0
    i = 5
    while i**2 <= x:
        if x % i == 0 or x % (i + 2) == 0:
            return 0
        i += 6
    return x
In [17]:
%%time

answer = 0

for i in range(1000000):
    answer += if_prime(i)

CPU times: user 3.48 s, sys: 0 ns, total: 3.48 s
Wall time: 3.48 s
In [18]:
%%time

if __name__ == '__main__':
    with Pool(2) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 114 ms, sys: 4.07 ms, total: 118 ms
Wall time: 1.91 s
In [19]:
%%time

if __name__ == '__main__':
    with Pool(4) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

CPU times: user 99.5 ms, sys: 30.5 ms, total: 130 ms
Wall time: 1.12 s
In [20]:
%%timeit

if __name__ == '__main__':
    with Pool(8) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

729 ms ± 3.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [21]:
%%timeit

if __name__ == '__main__':
    with Pool(16) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

512 ms ± 39.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [22]:
%%timeit

if __name__ == '__main__':
    with Pool(32) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

518 ms ± 13.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]:
%%timeit

if __name__ == '__main__':
    with Pool(64) as p:
        answer = sum(p.map(if_prime, list(range(1000000))))

621 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [ ]:

Los procesadores modernos tienen más de un núcleo, por lo que podemos acelerar las tareas utilizando el multiprocesamiento del módulo de procesamiento multiproceso . Las tareas asociadas con el procesador son programas que la mayoría de las veces trabajan realizan cálculos en el procesador (cálculos matemáticos triviales, procesamiento de imágenes, etc.). Si los cálculos pueden realizarse independientemente uno del otro, tenemos la oportunidad de dividirlos entre los núcleos de procesador disponibles, obteniendo así un aumento significativo en la velocidad de procesamiento.

Todo lo que tienes que hacer es:

  1. Determine qué función aplicar
  2. Prepare una lista de elementos a los que se aplicará la función;
  3. multiprocessing.Pool. , Pool(), . with , .
  4. Pool map. map , , .

Nota : Se puede definir una función para realizar cualquier tarea que se pueda realizar en paralelo. Por ejemplo, una función puede contener código para escribir el resultado de un cálculo en un archivo.

Entonces, ¿por qué necesitamos separarnosmultiprocessingymultithreading? Si alguna vez ha intentado mejorar el rendimiento de una tarea en el procesador utilizando subprocesos múltiples, el resultado es exactamente lo contrario. Eso es simplemente terrible! Veamos cómo sucedió.

Así como un mago está limitado por su naturaleza humana y solo puede calcular un número por unidad de tiempo, Python viene con una cosa llamada Global Interpreter Lock (GIL) . Python se complace en permitirte generar tantos hilos como quieras, pero GILasegura que solo uno de estos hilos se ejecutará en un momento dado.

Para una tarea relacionada con E / S, esta situación es completamente normal. Un hilo envía una solicitud a una URL y espera una respuesta, solo entonces este hilo puede ser reemplazado por otro, que enviará otra solicitud a una URL diferente. Dado que el subproceso no tiene que hacer nada hasta que reciba una respuesta, no importa que en el momento dado solo se esté ejecutando un subproceso.

Para las tareas realizadas en el procesador, tener múltiples hilos es casi tan inútil como los pezones en la armadura. Dado que solo se puede ejecutar un subproceso por unidad de tiempo, incluso si genera varios subprocesos, a cada uno de los cuales se le asignará un número para verificar la simplicidad, el procesador seguirá funcionando con un solo subproceso. De hecho, estos números aún se verificarán uno por uno. Y la sobrecarga de trabajar con múltiples subprocesos ayudará a reducir el rendimiento, que solo puede observar cuando se utiliza multithreadingen tareas realizadas en el procesador.

Para evitar esta "restricción", utilizamos el módulomultiprocessing. En lugar de usar hilos, el multiprocesamiento usa, como usted lo dice ... varios procesos. Cada proceso tiene su propio intérprete personal y espacio de memoria, por lo que el GIL no lo limitará. De hecho, cada proceso usará su propio núcleo de procesador y trabajará con su propio número único, y esto se realizará simultáneamente con el trabajo de otros procesos. ¡Qué dulce de su parte!

Puede notar que la carga de la CPU será mayor cuando la use multiprocessingen comparación con un ciclo regular o incluso multithreading. Esto sucede porque su programa no usa un núcleo, sino varios. Y esto es bueno!

recuerda esomultiprocessingTiene su propia sobrecarga de administrar múltiples procesos, que generalmente es más grave que el costo de subprocesos múltiples. (El multiprocesamiento genera intérpretes separados y asigna su propia área de memoria a cada proceso, ¡así que sí!) Es decir, por regla general, es mejor usar una versión ligera de subprocesamiento múltiple cuando desee salir de esta manera (recuerde las tareas relacionadas con E / S). Pero cuando el cálculo en el procesador se convierte en un cuello de botella, llega el momento del módulo multiprocessing. Pero recuerda que con un gran poder viene una gran responsabilidad.

Si genera más procesos de los que su procesador puede procesar por unidad de tiempo, notará que el rendimiento comenzará a disminuir. Esto sucede porque el sistema operativo necesita hacer más trabajo mezclando procesos entre los núcleos del procesador, porque hay más procesos. En realidad, todo puede ser aún más complicado de lo que te dije hoy, pero transmití la idea principal. Por ejemplo, en mi sistema, el rendimiento disminuirá cuando el número de procesos sea 16. Esto se debe a que solo hay 16 núcleos lógicos en mi procesador.

Capítulo 4: Conclusión


  • En tareas relacionadas con E / S, multithreadingpuede mejorar el rendimiento.
  • En tareas relacionadas con E / S, multiprocessingtambién puede aumentar la productividad, pero los costos, como regla, son más altos que cuando se usa multithreading.
  • La existencia de Python GIL nos deja en claro que en un momento dado de un programa, solo se puede ejecutar un subproceso.
  • En tareas relacionadas con el procesador, el uso multithreadingpuede reducir el rendimiento.
  • En las tareas relacionadas con el procesador, el uso multiprocessingpuede mejorar el rendimiento.
  • ¡Los magos son increíbles!

Ahí es donde vamos a terminar nuestra introducción multithreadingy multiprocessingen Python hoy . ¡Ahora ve y gana!



"Modelado de COVID-19 utilizando análisis de gráficos y análisis de datos abiertos". Lección gratis

All Articles