Greenlet Vs. Themen

141

Ich bin neu in Gevents und Greenlets. Ich habe eine gute Dokumentation gefunden, wie man mit ihnen arbeitet, aber keine hat mir Rechtfertigung dafür gegeben, wie und wann ich Greenlets verwenden sollte!

  • Was können sie wirklich gut?
  • Ist es eine gute Idee, sie auf einem Proxyserver zu verwenden oder nicht?
  • Warum nicht Threads?

Ich bin mir nicht sicher, wie sie uns Parallelität bieten können, wenn sie im Grunde genommen Co-Routinen sind.

Rsh
quelle
1
@ Imran Es geht um Greenthreads in Java. Meine Frage bezieht sich auf Greenlet in Python. Vermisse ich etwas
Rsh
Afaik, Threads in Python sind aufgrund der globalen Interpreter-Sperre eigentlich nicht wirklich gleichzeitig. Es würde sich also darauf beschränken, den Overhead beider Lösungen zu vergleichen. Obwohl ich verstehe, dass es mehrere Implementierungen von Python gibt, gilt dies möglicherweise nicht für alle.
Didierc
3
@didierc CPython (und PyPy ab sofort) interpretiert Python-Code (Byte) nicht parallel ( dh wirklich physisch gleichzeitig auf zwei verschiedenen CPU-Kernen). Allerdings befindet sich nicht alles, was ein Python-Programm tut, unter der GIL (gängige Beispiele sind Systemaufrufe, einschließlich E / A- und C-Funktionen, die die GIL absichtlich freigeben), und a threading.Threadist tatsächlich ein Betriebssystem-Thread mit allen Auswirkungen. So einfach ist das also wirklich nicht. Übrigens hat Jython keine GIL AFAIK und PyPy versucht es auch loszuwerden.

Antworten:

204

Greenlets bieten Parallelität, aber keine Parallelität. Parallelität ist, wenn Code unabhängig von anderem Code ausgeführt werden kann. Parallelität ist die gleichzeitige Ausführung von gleichzeitigem Code. Parallelität ist besonders nützlich, wenn im Benutzerbereich viel Arbeit zu erledigen ist, und das ist normalerweise CPU-lastiges Zeug. Parallelität ist nützlich, um Probleme zu lösen und verschiedene Teile parallel zu planen und einfacher zu verwalten.

Greenlets glänzen wirklich in der Netzwerkprogrammierung, bei der Interaktionen mit einem Socket unabhängig von Interaktionen mit anderen Sockets auftreten können. Dies ist ein klassisches Beispiel für Parallelität. Da jedes Greenlet in einem eigenen Kontext ausgeführt wird, können Sie weiterhin synchrone APIs ohne Threading verwenden. Dies ist gut, da Threads in Bezug auf den virtuellen Speicher und den Kernel-Overhead sehr teuer sind, sodass die Parallelität, die Sie mit Threads erzielen können, erheblich geringer ist. Darüber hinaus ist das Threading in Python aufgrund der GIL teurer und eingeschränkter als gewöhnlich. Alternativen zur Parallelität sind normalerweise Projekte wie Twisted, libevent, libuv, node.js usw., bei denen Ihr gesamter Code denselben Ausführungskontext verwendet, und die Registrierung von Ereignishandlern.

Es ist eine hervorragende Idee, Greenlets (mit entsprechender Netzwerkunterstützung, z. B. über gevent) zum Schreiben eines Proxys zu verwenden, da Ihre Bearbeitung von Anforderungen unabhängig ausgeführt werden kann und als solche geschrieben werden sollte.

Greenlets bieten Parallelität aus den Gründen, die ich zuvor angegeben habe. Parallelität ist keine Parallelität. Durch das Verbergen der Ereignisregistrierung und das Durchführen einer Zeitplanung für Anrufe, die normalerweise den aktuellen Thread blockieren würden, stellen Projekte wie gevent diese Parallelität offen, ohne dass eine Änderung an einer asynchronen API erforderlich ist, und dies zu erheblich geringeren Kosten für Ihr System.

Matt Joiner
quelle
1
Vielen Dank, nur zwei kleine Fragen: 1) Ist es möglich, diese Lösung mit Multiprocessing zu kombinieren, um einen höheren Durchsatz zu erzielen? 2) Ich weiß immer noch nicht, warum ich jemals Threads verwende. Können wir sie als naive und grundlegende Implementierung der Parallelität in der Python-Standardbibliothek betrachten?
Rsh
6
1) Ja, absolut. Sie sollten dies nicht vorzeitig tun, aber aufgrund einer ganzen Reihe von Faktoren, die über den Rahmen dieser Frage hinausgehen, erhalten Sie einen höheren Durchsatz, wenn mehrere Prozesse Anforderungen bedienen. 2) Betriebssystem-Threads werden präventiv geplant und standardmäßig vollständig parallelisiert. Sie sind die Standardeinstellung in Python, da Python die native Threading-Schnittstelle verfügbar macht und Threads der am besten unterstützte und kleinste gemeinsame Nenner für Parallelität und Parallelität in modernen Betriebssystemen sind.
Matt Joiner
6
Ich sollte erwähnen, dass Sie nicht einmal Greenlets verwenden sollten, bis die Threads nicht zufriedenstellend sind (normalerweise tritt dies aufgrund der Anzahl der gleichzeitigen Verbindungen auf, die Sie bearbeiten, und entweder die Anzahl der Threads oder die GIL bereiten Ihnen Kummer) und sogar dann nur, wenn Ihnen keine andere Option zur Verfügung steht. Die Python-Standardbibliothek und die meisten Bibliotheken von Drittanbietern erwarten, dass die Parallelität über Threads erreicht wird. Daher kann es zu merkwürdigem Verhalten kommen, wenn Sie dies über Greenlets bereitstellen.
Matt Joiner
18

Wenn Sie die Antwort von @ Max nehmen und sie für die Skalierung relevant machen, können Sie den Unterschied erkennen. Dies habe ich erreicht, indem ich die zu füllenden URLs wie folgt geändert habe:

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)

Ich musste die Multiprozess-Version fallen lassen, als sie fiel, bevor ich 500 hatte; aber bei 10.000 Iterationen:

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

Sie sehen also, dass es bei der Verwendung von gevent einen signifikanten Unterschied bei der E / A gibt

TemporalBeing
quelle
4
Es ist völlig falsch, 60000 native Threads oder Prozesse zu erzeugen, um die Arbeit abzuschließen, und dieser Test zeigt nichts an (haben Sie auch das Timeout für den Aufruf von gevent.joinall () entfernt?). Versuchen Sie es mit einem Thread-Pool von ca. 50 Threads, siehe meine Antwort: stackoverflow.com/a/51932442/34549
zzzeek
9

Wenn Sie die Antwort von @TemporalBeing oben korrigieren, sind Greenlets nicht "schneller" als Threads und es ist eine falsche Programmiertechnik, 60000 Threads zu erzeugen , um ein Parallelitätsproblem zu lösen. Stattdessen ist ein kleiner Pool von Threads angemessen. Hier ist ein vernünftigerer Vergleich (aus meinem reddit-Beitrag als Antwort auf Leute, die diesen SO-Beitrag zitieren).

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)

Hier sind einige Ergebnisse:

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

Das Missverständnis, das jeder über nicht blockierende E / A mit Python hat, ist die Überzeugung, dass der Python-Interpreter die Arbeit erledigen kann, Ergebnisse von Sockets in großem Maßstab schneller abzurufen, als die Netzwerkverbindungen selbst E / A zurückgeben können. Während dies in einigen Fällen sicherlich zutrifft, trifft es bei weitem nicht so oft zu, wie die Leute denken, da der Python-Interpreter sehr, sehr langsam ist. In meinem Blog-Beitrag hier illustriere ich einige grafische Profile, die zeigen, dass selbst für sehr einfache Dinge, wenn Sie mit einem klaren und schnellen Netzwerkzugriff auf Datenbanken oder DNS-Server zu tun haben, diese Dienste viel schneller als der Python-Code zurückkehren können kann sich um viele tausend dieser Verbindungen kümmern.

zzzeek
quelle
8

Das ist interessant genug, um es zu analysieren. Hier ist ein Code zum Vergleichen der Leistung von Greenlets im Vergleich zum Multiprocessing-Pool im Vergleich zum Multithreading:

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

Hier sind die Ergebnisse:

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

Ich denke, dass Greenlet behauptet, dass es im Gegensatz zur Multithreading-Bibliothek nicht an GIL gebunden ist. Darüber hinaus sagt Greenlet doc, dass es für den Netzwerkbetrieb gedacht ist. Für einen netzwerkintensiven Betrieb ist das Thread-Switching in Ordnung und Sie können sehen, dass der Multithreading-Ansatz ziemlich schnell ist. Außerdem ist es immer vorzuziehen, die offiziellen Bibliotheken von Python zu verwenden. Ich habe versucht, Greenlet unter Windows zu installieren, und bin auf ein DLL-Abhängigkeitsproblem gestoßen. Daher habe ich diesen Test unter Linux VM ausgeführt. Versuchen Sie immer, einen Code zu schreiben, in der Hoffnung, dass er auf jedem Computer ausgeführt wird.

max
quelle
25
Beachten Sie, dass getsockbynamedie Ergebnisse auf Betriebssystemebene zwischengespeichert werden (zumindest auf meinem Computer). Wenn es auf einem zuvor unbekannten oder abgelaufenen DNS aufgerufen wird, führt es tatsächlich eine Netzwerkabfrage durch, die einige Zeit dauern kann. Wenn ein Hostname aufgerufen wird, der erst kürzlich aufgelöst wurde, wird die Antwort viel schneller zurückgegeben. Folglich ist Ihre Messmethode hier fehlerhaft. Dies erklärt Ihre seltsamen Ergebnisse - Gevent kann nicht viel schlimmer sein als Multithreading - beide sind auf VM-Ebene nicht wirklich parallel.
KT.
1
@KT. das ist ein ausgezeichneter Punkt. Sie müssten diesen Test viele Male ausführen und Mittel, Modi und Mediane verwenden, um ein gutes Bild zu erhalten. Beachten Sie auch, dass Router Routenpfade für Protokolle zwischenspeichern und wenn sie Routenpfade nicht zwischenspeichern, kann es zu unterschiedlichen Verzögerungen durch unterschiedlichen DNS-Routenpfadverkehr kommen. Und DNS-Server zwischenspeichern stark. Es ist möglicherweise besser, das Threading mit time.clock () zu messen, wenn CPU-Zyklen verwendet werden, anstatt durch Latenz über Netzwerkhardware beeinflusst zu werden. Dies könnte dazu führen, dass sich andere Betriebssystemdienste nicht mehr einschleichen und mehr Zeit für Ihre Messungen benötigen.
DevPlayer
Oh, und Sie können zwischen diesen drei Tests einen DNS-Flush auf Betriebssystemebene ausführen, aber auch dies würde nur falsche Daten aus dem lokalen DNS-Caching reduzieren.
DevPlayer
Jep. Ausführen dieser bereinigten Version: paste.ubuntu.com/p/pg3KTzT2FG Ich bekomme ziemlich identische Zeiten ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe
Ich denke, OSX führt DNS-Caching durch, aber unter Linux ist dies keine "Standard" -Sache : stackoverflow.com/a/11021207/34549 , also ja, bei geringer Parallelität sind Greenlets aufgrund des Interpreter-Overheads
umso