Greenlet Vs. Wątki

141

Jestem nowy w gevents i greenlets. Znalazłem dobrą dokumentację, jak z nimi pracować, ale żadna nie podała mi uzasadnienia, jak i kiedy powinienem używać zielonych ulotek!

  • W czym są naprawdę dobrzy?
  • Czy warto używać ich na serwerze proxy, czy nie?
  • Dlaczego nie wątki?

Nie jestem pewien, w jaki sposób mogą zapewnić nam współbieżność, jeśli są w zasadzie współprogramami.

Rsh
źródło
1
@Imran Chodzi o greenthreads w Javie. Moje pytanie dotyczy Greenleta w Pythonie. Czy coś mi brakuje?
Rsh
Afaik, wątki w Pythonie nie są tak naprawdę współbieżne z powodu globalnej blokady interpretera. Sprowadziłoby się więc do porównania narzutów obu rozwiązań. Chociaż rozumiem, że istnieje kilka implementacji Pythona, więc może to nie dotyczyć wszystkich z nich.
didierc
3
@didierc CPython (i PyPy od teraz) nie będzie interpretować kodu Pythona (bajtowego) równolegle (to znaczy naprawdę fizycznie w tym samym czasie na dwóch różnych rdzeniach procesora). Jednak nie wszystko, co robi program w Pythonie, podlega GIL (typowymi przykładami są wywołania systemowe, w tym funkcje I / O i C, które celowo zwalniają GIL), a threading.Threadjest w rzeczywistości wątkiem systemu operacyjnego ze wszystkimi konsekwencjami. Więc to naprawdę nie jest takie proste. Nawiasem mówiąc, Jython nie ma GIL AFAIK, a PyPy również próbuje się go pozbyć.

Odpowiedzi:

204

Greenlets zapewniają współbieżność, ale nie równoległość. Współbieżność występuje wtedy, gdy kod może działać niezależnie od innego kodu. Paralelizm to jednoczesne wykonywanie kodu współbieżnego. Równoległość jest szczególnie przydatna, gdy w przestrzeni użytkownika jest dużo pracy do wykonania, a jest to typowo obciążająca procesor. Współbieżność jest przydatna do rozdzielania problemów, umożliwiając równoległe planowanie i zarządzanie różnymi częściami.

Greenlets naprawdę błyszczą w programowaniu sieciowym, w którym interakcje z jednym gniazdem mogą zachodzić niezależnie od interakcji z innymi gniazdami. To jest klasyczny przykład współbieżności. Ponieważ każda zielona broszura działa w swoim własnym kontekście, możesz nadal używać synchronicznych interfejsów API bez tworzenia wątków. Jest to dobre, ponieważ wątki są bardzo drogie pod względem pamięci wirtualnej i narzutu jądra, więc współbieżność, którą można osiągnąć z wątkami, jest znacznie mniejsza. Ponadto wątki w Pythonie są droższe i bardziej ograniczone niż zwykle ze względu na GIL. Alternatywą dla współbieżności są zwykle projekty takie jak Twisted, libevent, libuv, node.js itp., W których cały kod ma ten sam kontekst wykonywania i rejestruje programy obsługi zdarzeń.

Doskonałym pomysłem jest używanie zielonych ulotek (z odpowiednim wsparciem sieciowym, takim jak gevent) do pisania proxy, ponieważ obsługa żądań może być wykonywana niezależnie i powinna być napisana jako taka.

Greenlety zapewniają współbieżność z powodów, które podałem wcześniej. Współbieżność nie jest równoległością. Ukrywając rejestrację zdarzeń i wykonując planowanie za Ciebie dla wywołań, które normalnie blokowałyby bieżący wątek, projekty takie jak gevent ujawniają tę współbieżność bez konieczności zmiany na asynchroniczny interfejs API i przy znacznie niższych kosztach dla systemu.

Matt Joiner
źródło
1
Dzięki, tylko dwa małe pytania: 1) Czy można połączyć to rozwiązanie z przetwarzaniem wieloprocesowym, aby osiągnąć większą przepustowość? 2) Nadal nie wiem, dlaczego kiedykolwiek korzystam z wątków? Czy możemy je uznać za naiwną i podstawową implementację współbieżności w standardowej bibliotece Pythona?
Rsh
6
1) Tak, absolutnie. Nie powinieneś tego robić przedwcześnie, ale z powodu całego szeregu czynników wykraczających poza zakres tego pytania, posiadanie wielu procesów obsługujących żądania zapewni wyższą przepustowość. 2) Wątki systemu operacyjnego są planowane z wyprzedzeniem i domyślnie są w pełni równoległe. Są domyślne w Pythonie, ponieważ Python udostępnia natywny interfejs wątków, a wątki są najlepiej obsługiwanym i najniższym wspólnym mianownikiem zarówno dla równoległości, jak i współbieżności w nowoczesnych systemach operacyjnych.
Matt Joiner,
6
Powinienem wspomnieć, że nie powinieneś nawet używać greenlets, dopóki wątki nie będą satysfakcjonujące (zwykle dzieje się tak z powodu liczby jednoczesnych połączeń, które obsługujesz, a liczba wątków lub GIL przysparza Ci smutku), a nawet wtedy tylko wtedy, gdy nie masz innej dostępnej opcji. Biblioteka standardowa Pythona i większość bibliotek innych firm oczekuje, że współbieżność zostanie osiągnięta za pośrednictwem wątków, więc możesz uzyskać dziwne zachowanie, jeśli udostępnisz to za pośrednictwem zielonych ulotek.
Matt Joiner,
@MattJoiner Mam poniżej funkcję, która czyta ogromny plik, aby obliczyć sumę md5. jak mogę użyć gevent w tym przypadku, aby czytać szybciej 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

Biorąc odpowiedź @ Maxa i dodając do niej znaczenie przy skalowaniu, możesz zobaczyć różnicę. Osiągnąłem to, zmieniając adresy URL do wypełnienia w następujący sposób:

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)

Musiałem zrezygnować z wersji wieloprocesowej, ponieważ spadła, zanim miałem 500; ale przy 10.000 iteracjach:

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

Możesz więc zobaczyć, że istnieje znacząca różnica we / wy przy użyciu gevent

Czasowe Istnienie
źródło
4
zrobienie 60000 natywnych wątków lub procesów w celu zakończenia pracy jest całkowicie niepoprawne, a ten test nic nie pokazuje (również czy usunąłeś limit czasu z wywołania gevent.joinall ()?). Spróbuj użyć puli wątków składającej się z około 50 wątków, zobacz moją odpowiedź: stackoverflow.com/a/51932442/34549
zzzeek
9

Poprawiając powyższą odpowiedź @TemporalBeinga, greenlety nie są „szybsze” niż wątki i jest nieprawidłową techniką programowania, aby spawnować 60000 wątków w celu rozwiązania problemu współbieżności. Zamiast tego odpowiednia jest mała pula wątków. Oto bardziej rozsądne porównanie (z mojego posta reddit w odpowiedzi na osoby cytujące ten 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)

Oto kilka wyników:

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

nieporozumieniem, jakie wszyscy mają na temat nieblokującego wejścia / wyjścia w Pythonie, jest przekonanie, że interpreter języka Python może zająć się pobieraniem wyników z gniazd na dużą skalę, szybciej niż same połączenia sieciowe mogą zwrócić IO. Chociaż jest to z pewnością prawdą w niektórych przypadkach, nie jest to prawdą prawie tak często, jak ludzie myślą, ponieważ interpreter Pythona jest naprawdę, bardzo powolny. W moim wpisie na blogu ilustruję kilka profili graficznych, które pokazują, że nawet w przypadku bardzo prostych rzeczy, jeśli masz do czynienia z wyraźnym i szybkim dostępem sieciowym do takich rzeczy, jak bazy danych lub serwery DNS, usługi te mogą wrócić o wiele szybciej niż kod Pythona może obsłużyć wiele tysięcy takich połączeń.

zzzeek
źródło
8

Jest to wystarczająco interesujące do przeanalizowania. Oto kod porównujący wydajność greenlets z pulą wieloprocesorową i wielowątkowością:

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

Oto wyniki:

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

Myślę, że Greenlet twierdzi, że nie jest związany GIL w przeciwieństwie do biblioteki wielowątkowej. Co więcej, doc Greenlet mówi, że jest przeznaczony do operacji sieciowych. W przypadku intensywnych operacji sieciowych przełączanie wątków jest dobre i widać, że podejście wielowątkowe jest dość szybkie. Ponadto zawsze lepiej jest używać oficjalnych bibliotek Pythona; Próbowałem zainstalować Greenlet w systemie Windows i napotkałem problem z zależnościami dll, więc przeprowadziłem ten test na maszynie wirtualnej z systemem Linux. Zawsze staraj się pisać kod z nadzieją, że będzie działał na dowolnej maszynie.

max
źródło
25
Zauważ, że getsockbynamebuforuje wyniki na poziomie systemu operacyjnego (przynajmniej na moim komputerze tak robi). Po wywołaniu na wcześniej nieznanym lub wygasłym serwerze DNS, faktycznie wykona zapytanie sieciowe, co może zająć trochę czasu. Wywołane dla nazwy hosta, która niedawno została rozwiązana, zwróci odpowiedź znacznie szybciej. W rezultacie twoja metodologia pomiaru jest tutaj błędna. To wyjaśnia twoje dziwne wyniki - gevent nie może być tak naprawdę gorszy niż wielowątkowość - oba nie są tak naprawdę równoległe na poziomie VM.
KT.
1
@KT. to doskonała uwaga. Aby uzyskać dobry obraz, należałoby uruchomić ten test wiele razy i wziąć średnie, tryby i mediany. Zauważ również, że routery buforują ścieżki tras dla protokołów, a jeśli nie buforują ścieżek tras, możesz uzyskać różne opóźnienia z ruchu na różnych trasach DNS. Serwery dns mocno buforują pamięć podręczną. Lepszym rozwiązaniem może być mierzenie wątków przy użyciu metody time.clock (), w której cykle procesora są używane, a nie opóźnienie sprzętu sieciowego. Może to wyeliminować wkradanie się innych usług systemu operacyjnego i wydłużanie czasu pomiarów.
DevPlayer
Aha i możesz uruchomić spłukiwanie DNS na poziomie systemu operacyjnego między tymi trzema testami, ale znowu zmniejszy to tylko fałszywe dane z lokalnego buforowania DNS.
DevPlayer
Tak. Uruchomienie tego czyszczona aż wersję: paste.ubuntu.com/p/pg3KTzT2FG mam prawie identyczne-owski razy ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe
Myślę, że OSX robi buforowanie dns, ale w Linuksie nie jest to „domyślna” rzecz: stackoverflow.com/a/11021207/34549 , więc tak, przy niskim poziomie współbieżności greenlets są o wiele gorsze ze względu na obciążenie interpretera
zzzeek