Greenlet contre. Fils

141

Je suis nouveau dans les gevents et les greenlets. J'ai trouvé une bonne documentation sur la façon de travailler avec eux, mais aucun ne m'a donné de justification sur comment et quand je devrais utiliser des greenlets!

  • Dans quoi sont-ils vraiment bons?
  • Est-ce une bonne idée de les utiliser dans un serveur proxy ou non?
  • Pourquoi pas des fils?

Ce dont je ne suis pas sûr, c'est de savoir comment ils peuvent nous fournir une concurrence d'accès s'ils sont essentiellement des co-routines.

Rsh
la source
1
@Imran Il s'agit de greenthreads en Java. Ma question concerne le greenlet en Python. Est-ce que je manque quelque chose?
Rsh
Afaik, les threads en python ne sont en fait pas vraiment simultanés à cause du verrouillage global de l'interpréteur. Cela reviendrait donc à comparer les frais généraux des deux solutions. Bien que je comprenne qu'il existe plusieurs implémentations de python, cela peut ne pas s'appliquer à toutes.
didierc
3
@didierc CPython (et PyPy pour le moment) n'interprétera pas le code Python (octet) en parallèle (c'est-à-dire vraiment physiquement en même temps sur deux cœurs de processeur distincts). Cependant, tout ce qu'un programme Python fait n'est pas sous le GIL (des exemples courants sont des appels système comprenant des fonctions d'E / S et C qui libèrent délibérément le GIL), et a threading.Threadest en fait un thread OS avec toutes les ramifications. Ce n'est donc vraiment pas aussi simple. Au fait, Jython n'a pas de GIL AFAIK et PyPy essaie de s'en débarrasser aussi.

Réponses:

204

Les greenlets fournissent la concurrence mais pas le parallélisme. La concurrence est le moment où le code peut s'exécuter indépendamment d'un autre code. Le parallélisme est l'exécution simultanée de code concurrent. Le parallélisme est particulièrement utile lorsqu'il y a beaucoup de travail à faire dans l'espace utilisateur, et c'est généralement un travail lourd en CPU. La simultanéité est utile pour séparer les problèmes, ce qui permet de planifier et de gérer plus facilement différentes parties en parallèle.

Les greenlets brillent vraiment dans la programmation réseau où les interactions avec un socket peuvent se produire indépendamment des interactions avec d'autres sockets. Ceci est un exemple classique de concurrence. Étant donné que chaque greenlet s'exécute dans son propre contexte, vous pouvez continuer à utiliser des API synchrones sans thread. C'est une bonne chose car les threads sont très chers en termes de mémoire virtuelle et de surcharge du noyau, de sorte que la concurrence que vous pouvez obtenir avec les threads est nettement inférieure. De plus, le threading en Python est plus cher et plus limité que d'habitude en raison du GIL. Les alternatives à la concurrence sont généralement des projets comme Twisted, libevent, libuv, node.js, etc., où tout votre code partage le même contexte d'exécution et enregistre les gestionnaires d'événements.

C'est une excellente idée d'utiliser des greenlets (avec un support réseau approprié tel que via gevent) pour écrire un proxy, car votre gestion des requêtes peut s'exécuter indépendamment et doit être écrite comme telle.

Les greenlets fournissent la concurrence pour les raisons que j'ai données plus tôt. La concurrence n'est pas le parallélisme. En masquant l'enregistrement des événements et en effectuant la planification pour vous sur les appels qui bloqueraient normalement le thread actuel, des projets comme gevent exposent cette concurrence sans nécessiter de modification d'une API asynchrone, et à un coût nettement inférieur pour votre système.

Matt Joiner
la source
1
Merci, juste deux petites questions: 1) Est-il possible de combiner cette solution avec le multitraitement pour atteindre un débit plus élevé? 2) Je ne sais toujours pas pourquoi utiliser des threads? Pouvons-nous les considérer comme une implémentation naïve et basique de la concurrence dans la bibliothèque standard python?
Rsh
6
1) Oui, absolument. Vous ne devriez pas le faire prématurément, mais en raison de tout un tas de facteurs dépassant la portée de cette question, le fait d'avoir plusieurs processus pour répondre aux demandes vous donnera un débit plus élevé. 2) Les threads du système d'exploitation sont planifiés de manière préemptive et entièrement parallélisés par défaut. Ils sont la valeur par défaut dans Python car Python expose l'interface de threading native, et les threads sont le dénominateur commun le mieux pris en charge et le plus bas pour le parallélisme et la concurrence dans les systèmes d'exploitation modernes.
Matt Joiner
6
Je dois mentionner que vous ne devriez même pas utiliser de greenlets tant que les threads ne sont pas satisfaisants (cela se produit généralement en raison du nombre de connexions simultanées que vous gérez, et soit le nombre de threads soit le GIL vous donnent du chagrin), et même alors seulement s'il n'y a pas d'autre option disponible pour vous. La bibliothèque standard Python et la plupart des bibliothèques tierces s'attendent à ce que la concurrence soit obtenue via des threads, vous pouvez donc avoir un comportement étrange si vous le fournissez via des greenlets.
Matt Joiner
@MattJoiner J'ai la fonction ci-dessous qui lit l'énorme fichier pour calculer la somme md5. comment puis-je utiliser gevent dans ce cas pour lire plus rapidement 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

En prenant la réponse de @ Max et en y ajoutant une certaine pertinence pour la mise à l'échelle, vous pouvez voir la différence. J'ai réalisé cela en modifiant les URL à remplir comme suit:

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)

J'ai dû abandonner la version multiprocessus car elle tombait avant d'en avoir 500; mais à 10 000 itérations:

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

Vous pouvez donc voir qu'il y a une différence significative dans les E / S en utilisant gevent

TemporalEtre
la source
4
il est tout à fait incorrect de générer 60000 threads ou processus natifs pour terminer le travail et ce test ne montre rien (avez-vous également pris le délai d'attente de l'appel gevent.joinall ()?). Essayez d'utiliser un pool de threads d'environ 50 threads, voir ma réponse: stackoverflow.com/a/51932442/34549
zzzeek
9

En corrigeant la réponse de @TemporalBeing ci-dessus, les greenlets ne sont pas "plus rapides" que les threads et c'est une technique de programmation incorrecte de générer 60000 threads pour résoudre un problème de concurrence, un petit pool de threads est plutôt approprié. Voici une comparaison plus raisonnable (de mon post reddit en réponse aux personnes citant ce post 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)

Voici quelques résultats:

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

le malentendu que tout le monde a à propos des E / S non bloquantes avec Python est la croyance que l'interpréteur Python peut s'occuper du travail de récupération des résultats à partir des sockets à grande échelle plus rapidement que les connexions réseau elles-mêmes ne peuvent renvoyer des E / S. Bien que cela soit certainement vrai dans certains cas, ce n'est pas aussi souvent que les gens le pensent, car l'interpréteur Python est vraiment très lent. Dans mon article de blog ici , j'illustre certains profils graphiques qui montrent que même pour des choses très simples, si vous avez affaire à un accès réseau net et rapide à des éléments tels que des bases de données ou des serveurs DNS, ces services peuvent revenir beaucoup plus rapidement que le code Python. peut s'occuper de plusieurs milliers de ces connexions.

zzzeek
la source
8

C'est assez intéressant pour être analysé. Voici un code pour comparer les performances des greenlets par rapport au pool multitraitement par rapport au 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()

Voici les résultats:

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

Je pense que greenlet prétend qu'il n'est pas lié par GIL contrairement à la bibliothèque multithreading. De plus, le document Greenlet dit qu'il est destiné aux opérations réseau. Pour une opération réseau intensive, la commutation de threads est très bien et vous pouvez voir que l'approche multithreading est assez rapide. De plus, il est toujours préférable d'utiliser les bibliothèques officielles de python; J'ai essayé d'installer greenlet sur Windows et j'ai rencontré un problème de dépendance dll, j'ai donc exécuté ce test sur une machine virtuelle Linux. Essayez toujours d'écrire un code dans l'espoir qu'il fonctionne sur n'importe quelle machine.

max
la source
25
Notez que getsockbynamemet en cache les résultats au niveau du système d'exploitation (du moins sur ma machine, c'est le cas). Lorsqu'il est appelé sur un DNS précédemment inconnu ou expiré, il effectuera en fait une requête réseau, ce qui peut prendre un certain temps. Lorsqu'il est appelé sur un nom d'hôte qui vient d'être résolu, il renvoie la réponse beaucoup plus rapidement. Par conséquent, votre méthodologie de mesure est ici défectueuse. Cela explique vos résultats étranges - gevent ne peut pas vraiment être bien pire que le multithreading - les deux ne sont pas vraiment parallèles au niveau de la VM.
KT.
1
@KT. c'est un excellent point. Vous devrez exécuter ce test plusieurs fois et prendre des moyens, des modes et des médianes pour obtenir une bonne image. Notez également que les routeurs mettent en cache les chemins de route pour les protocoles et là où ils ne mettent pas en cache les chemins de route, vous pouvez obtenir un décalage différent du trafic de chemin de route DNS différent. Et les serveurs DNS mettent fortement en cache. Il peut être préférable de mesurer le threading à l'aide de time.clock () où les cycles du processeur sont utilisés au lieu d'être affectés par la latence sur le matériel réseau. Cela pourrait éliminer d'autres services du système d'exploitation qui se faufilent et ajoutent du temps à vos mesures.
DevPlayer
Oh et vous pouvez exécuter un vidage DNS au niveau du système d'exploitation entre ces trois tests, mais encore une fois, cela ne ferait que réduire les fausses données de la mise en cache DNS locale.
DevPlayer
Ouaip. Exécution de cette version assainis: paste.ubuntu.com/p/pg3KTzT2FG je reçois à peu près identiques fois-ish ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe
Je pense qu'OSX fait de la mise en cache DNS mais sous Linux ce n'est pas une chose "par défaut": stackoverflow.com/a/11021207/34549 , donc oui, à de faibles niveaux de concurrence, les greenlets sont bien pires en raison de la surcharge de l'interpréteur
zzzeek