Greenlet vs. Hilos

141

Soy nuevo en gevents y greenlets. ¡Encontré una buena documentación sobre cómo trabajar con ellos, pero ninguno me dio justificación sobre cómo y cuándo debería usar greenlets!

  • ¿En qué son realmente buenos?
  • ¿Es una buena idea usarlos en un servidor proxy o no?
  • ¿Por qué no hilos?

De lo que no estoy seguro es de cómo nos pueden proporcionar concurrencia si son básicamente co-rutinas.

Rsh
fuente
1
@ Imran Se trata de greenthreads en Java. Mi pregunta es sobre greenlet en Python. Me estoy perdiendo de algo ?
Rsh
Afaik, los hilos en python en realidad no son realmente concurrentes debido al bloqueo global del intérprete. Por lo tanto, se reduciría a comparar los gastos generales de ambas soluciones. Aunque entiendo que hay varias implementaciones de python, es posible que esto no se aplique a todas.
didierc
3
@didierc CPython (y PyPy a partir de ahora) no interpretará el código Python (byte) en paralelo (es decir, realmente físicamente al mismo tiempo en dos núcleos de CPU distintos). Sin embargo, no todo lo que hace un programa Python está bajo el GIL (ejemplos comunes son llamadas al sistema, incluidas las funciones de E / S y C que liberan deliberadamente el GIL), y en threading.Threadrealidad es un hilo del sistema operativo con todas las ramificaciones. Entonces, en realidad no es tan simple. Por cierto, Jython no tiene GIL AFAIK y PyPy también está tratando de deshacerse de él.

Respuestas:

204

Los greenlets proporcionan concurrencia pero no paralelismo. La concurrencia es cuando el código puede ejecutarse independientemente de otro código. El paralelismo es la ejecución de código concurrente simultáneamente. El paralelismo es particularmente útil cuando hay mucho trabajo por hacer en el espacio de usuario, y eso es típicamente un trabajo pesado de CPU. La concurrencia es útil para resolver problemas, ya que permite programar y gestionar diferentes partes más fácilmente en paralelo.

Los greenlets realmente brillan en la programación de red donde las interacciones con un socket pueden ocurrir independientemente de las interacciones con otros sockets. Este es un ejemplo clásico de concurrencia. Debido a que cada greenlet se ejecuta en su propio contexto, puede continuar utilizando API síncronas sin subprocesos. Esto es bueno porque los subprocesos son muy caros en términos de memoria virtual y sobrecarga del núcleo, por lo que la concurrencia que puede lograr con los subprocesos es significativamente menor. Además, el enhebrado en Python es más costoso y más limitado de lo habitual debido al GIL. Las alternativas a la concurrencia generalmente son proyectos como Twisted, libevent, libuv, node.js, etc., donde todo su código comparte el mismo contexto de ejecución y registra controladores de eventos.

Es una excelente idea usar greenlets (con soporte de red apropiado, como a través de gevent) para escribir un proxy, ya que su manejo de solicitudes puede ejecutarse de forma independiente y debe escribirse como tal.

Los Greenlets proporcionan concurrencia por las razones que expuse anteriormente. La concurrencia no es paralelismo. Al ocultar el registro de eventos y realizar la programación para usted en llamadas que normalmente bloquearían el hilo actual, proyectos como gevent exponen esta concurrencia sin requerir cambios a una API asincrónica, y a un costo significativamente menor para su sistema.

Matt Joiner
fuente
1
Gracias, solo dos pequeñas preguntas: 1) ¿Es posible combinar esta solución con multiprocesamiento para lograr un mayor rendimiento? 2) Todavía no sé por qué usar hilos? ¿Podemos considerarlos como una implementación ingenua y básica de concurrencia en la biblioteca estándar de Python?
Rsh
66
1) Sí, absolutamente. No debe hacer esto prematuramente, pero debido a una gran cantidad de factores más allá del alcance de esta pregunta, tener múltiples procesos para atender solicitudes le dará un mayor rendimiento. 2) Los subprocesos del sistema operativo se programan de forma preventiva y están totalmente paralelos de forma predeterminada. Son los valores predeterminados en Python porque Python expone la interfaz de subprocesos nativa, y los subprocesos son el mejor denominador común y el más bajo soporte para paralelismo y concurrencia en los sistemas operativos modernos.
Matt Joiner
66
Debo mencionar que ni siquiera deberías usar greenlets hasta que los hilos no sean satisfactorios (por lo general, esto ocurre debido a la cantidad de conexiones simultáneas que estás manejando, y el conteo de hilos o el GIL te están causando dolor), e incluso entonces solo si no hay otra opción disponible para usted. La biblioteca estándar de Python y la mayoría de las bibliotecas de terceros esperan que se logre la concurrencia a través de subprocesos, por lo que puede obtener un comportamiento extraño si lo proporciona a través de greenlets.
Matt Joiner
@MattJoiner Tengo la siguiente función que lee el archivo enorme para calcular la suma md5. ¿Cómo puedo usar gevent en este caso para leer más rápido? import hashlib def checksum_md5(filename): md5 = hashlib.md5() with open(filename,'rb') as f: for chunk in iter(lambda: f.read(8192), b''): md5.update(chunk) return md5.digest()
Soumya
18

Tomando la respuesta de @ Max y agregando cierta relevancia para el escalado, puede ver la diferencia. Lo logré cambiando las URL para que se llenen de la siguiente manera:

URLS_base = ['www.google.com', 'www.example.com', 'www.python.org', 'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']
URLS = []
for _ in range(10000):
    for url in URLS_base:
        URLS.append(url)

Tuve que abandonar la versión multiproceso ya que cayó antes de tener 500; pero a 10,000 iteraciones:

Using gevent it took: 3.756914
-----------
Using multi-threading it took: 15.797028

Entonces puede ver que hay una diferencia significativa en E / S usando gevent

TemporalSer
fuente
44
es completamente incorrecto generar 60000 hilos o procesos nativos para completar el trabajo y esta prueba no muestra nada (¿también eliminó el tiempo de espera de la llamada gevent.joinall ()?). Intente usar un grupo de subprocesos de aproximadamente 50 subprocesos, vea mi respuesta: stackoverflow.com/a/51932442/34549
zzzeek
9

Al corregir la respuesta de @TemporalBeing anterior, los greenlets no son "más rápidos" que los hilos y es una técnica de programación incorrecta generar 60000 hilos para resolver un problema de concurrencia, en cambio es apropiado un pequeño grupo de hilos. Aquí hay una comparación más razonable (de mi publicación de reddit en respuesta a las personas que citan esta publicación SO).

import gevent
from gevent import socket as gsock
import socket as sock
import threading
from datetime import datetime


def timeit(fn, URLS):
    t1 = datetime.now()
    fn()
    t2 = datetime.now()
    print(
        "%s / %d hostnames, %s seconds" % (
            fn.__name__,
            len(URLS),
            (t2 - t1).total_seconds()
        )
    )


def run_gevent_without_a_timeout():
    ip_numbers = []

    def greenlet(domain_name):
        ip_numbers.append(gsock.gethostbyname(domain_name))

    jobs = [gevent.spawn(greenlet, domain_name) for domain_name in URLS]
    gevent.joinall(jobs)
    assert len(ip_numbers) == len(URLS)


def run_threads_correctly():
    ip_numbers = []

    def process():
        while queue:
            try:
                domain_name = queue.pop()
            except IndexError:
                pass
            else:
                ip_numbers.append(sock.gethostbyname(domain_name))

    threads = [threading.Thread(target=process) for i in range(50)]

    queue = list(URLS)
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    assert len(ip_numbers) == len(URLS)

URLS_base = ['www.google.com', 'www.example.com', 'www.python.org',
             'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']

for NUM in (5, 50, 500, 5000, 10000):
    URLS = []

    for _ in range(NUM):
        for url in URLS_base:
            URLS.append(url)

    print("--------------------")
    timeit(run_gevent_without_a_timeout, URLS)
    timeit(run_threads_correctly, URLS)

Aquí hay algunos resultados:

--------------------
run_gevent_without_a_timeout / 30 hostnames, 0.044888 seconds
run_threads_correctly / 30 hostnames, 0.019389 seconds
--------------------
run_gevent_without_a_timeout / 300 hostnames, 0.186045 seconds
run_threads_correctly / 300 hostnames, 0.153808 seconds
--------------------
run_gevent_without_a_timeout / 3000 hostnames, 1.834089 seconds
run_threads_correctly / 3000 hostnames, 1.569523 seconds
--------------------
run_gevent_without_a_timeout / 30000 hostnames, 19.030259 seconds
run_threads_correctly / 30000 hostnames, 15.163603 seconds
--------------------
run_gevent_without_a_timeout / 60000 hostnames, 35.770358 seconds
run_threads_correctly / 60000 hostnames, 29.864083 seconds

El malentendido que todo el mundo tiene acerca de no bloquear IO con Python es la creencia de que el intérprete de Python puede atender el trabajo de recuperar resultados de sockets a gran escala más rápido de lo que las conexiones de red pueden devolver IO. Si bien esto es cierto en algunos casos, no es cierto con tanta frecuencia como la gente piensa, porque el intérprete de Python es muy, muy lento. En mi blog aquí , ilustramos algunos perfiles gráficos que muestran que incluso para cosas muy simples, si se trata de un acceso nítido y rápido a redes como bases de datos o servidores DNS, esos servicios pueden volver mucho más rápido que el código Python puede atender a miles de esas conexiones.

zzzeek
fuente
8

Esto es lo suficientemente interesante como para analizar. Aquí hay un código para comparar el rendimiento de los greenlets versus el pool de multiprocesamiento versus multi-threading:

import gevent
from gevent import socket as gsock
import socket as sock
from multiprocessing import Pool
from threading import Thread
from datetime import datetime

class IpGetter(Thread):
    def __init__(self, domain):
        Thread.__init__(self)
        self.domain = domain
    def run(self):
        self.ip = sock.gethostbyname(self.domain)

if __name__ == "__main__":
    URLS = ['www.google.com', 'www.example.com', 'www.python.org', 'www.yahoo.com', 'www.ubc.ca', 'www.wikipedia.org']
    t1 = datetime.now()
    jobs = [gevent.spawn(gsock.gethostbyname, url) for url in URLS]
    gevent.joinall(jobs, timeout=2)
    t2 = datetime.now()
    print "Using gevent it took: %s" % (t2-t1).total_seconds()
    print "-----------"
    t1 = datetime.now()
    pool = Pool(len(URLS))
    results = pool.map(sock.gethostbyname, URLS)
    t2 = datetime.now()
    pool.close()
    print "Using multiprocessing it took: %s" % (t2-t1).total_seconds()
    print "-----------"
    t1 = datetime.now()
    threads = []
    for url in URLS:
        t = IpGetter(url)
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
    t2 = datetime.now()
    print "Using multi-threading it took: %s" % (t2-t1).total_seconds()

aquí están los resultados:

Using gevent it took: 0.083758
-----------
Using multiprocessing it took: 0.023633
-----------
Using multi-threading it took: 0.008327

Creo que Greenlet afirma que no está obligado por GIL a diferencia de la biblioteca de subprocesos múltiples. Además, el documento de Greenlet dice que está destinado a operaciones de red. Para una operación intensiva de red, el cambio de subprocesos está bien y puede ver que el enfoque de subprocesamiento múltiple es bastante rápido. Además, siempre es preferible utilizar las bibliotecas oficiales de Python; Intenté instalar greenlet en Windows y encontré un problema de dependencia de dll, así que ejecuté esta prueba en un linux vm. Siempre trate de escribir un código con la esperanza de que se ejecute en cualquier máquina.

max
fuente
25
Tenga en cuenta que getsockbynamealmacena en caché los resultados a nivel del sistema operativo (al menos en mi máquina). Cuando se invoca en un DNS previamente desconocido o caducado, en realidad realizará una consulta de red, lo que puede llevar algún tiempo. Cuando se invoca en un nombre de host que se ha resuelto recientemente, devolverá la respuesta mucho más rápido. En consecuencia, su metodología de medición es defectuosa aquí. Esto explica sus resultados extraños: gevent realmente no puede ser mucho peor que el subprocesamiento múltiple; ambos no son realmente paralelos a nivel de VM.
KT.
1
@KT. Ese es un excelente punto. Tendría que ejecutar esa prueba muchas veces y tomar medios, modos y medianas para obtener una buena imagen. Tenga en cuenta también que los enrutadores almacenan en caché las rutas de ruta para los protocolos y donde no almacenan en caché las rutas de ruta, puede obtener un retraso diferente del tráfico de ruta de ruta de DNS diferente. Y los servidores dns en gran medida de caché. Podría ser mejor medir el enhebrado usando time.clock () donde se utilizan ciclos de CPU en lugar de verse afectados por la latencia sobre el hardware de la red. Esto podría eliminar otros servicios del sistema operativo y colarse y agregar tiempo a sus mediciones.
DevPlayer
Ah, y puede ejecutar una descarga de DNS en el nivel del sistema operativo entre esas tres pruebas, pero de nuevo eso solo reduciría los datos falsos del almacenamiento en caché de DNS local.
DevPlayer
Sip. Ejecutando esta versión limpia: paste.ubuntu.com/p/pg3KTzT2FG Obtengo tiempos casi idénticos ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe
Creo que OSX está haciendo almacenamiento en caché de dns, pero en Linux no es una cosa "predeterminada": stackoverflow.com/a/11021207/34549 , así que sí, a niveles bajos de simultaneidad, los greenlets son mucho peores debido a la sobrecarga del intérprete
zzzeek