Greenlet vs. discussioni


141

Sono nuovo di gevents e greenlets. Ho trovato una buona documentazione su come lavorare con loro, ma nessuno mi ha dato giustificazioni su come e quando dovrei usare i greenlet!

  • In cosa sono veramente bravi?
  • È una buona idea usarli in un server proxy o no?
  • Perché non le discussioni?

Ciò di cui non sono sicuro è come possono fornirci la concorrenza se sono fondamentalmente co-routine.


1
@Imran Si tratta di greenthreads in Java. La mia domanda riguarda greenlet in Python. Mi sto perdendo qualcosa ?
Rsh

Dopo tutto, i thread in Python non sono realmente simultanei a causa del blocco dell'interprete globale. Quindi si ridurrebbe a confrontare le spese generali di entrambe le soluzioni. Anche se capisco che ci sono diverse implementazioni di Python, quindi questo potrebbe non valere per tutti.
didierc,

3
@didierc CPython (e PyPy a partire da ora) non interpreteranno il codice Python (byte) in parallelo (cioè, fisicamente allo stesso tempo su due core CPU distinti). Tuttavia, non tutto ciò che fa un programma Python è sotto GIL (esempi comuni sono syscalls tra cui funzioni I / O e C che rilasciano deliberatamente GIL), e a threading.Threadè in realtà un thread del sistema operativo con tutte le ramificazioni. Quindi non è proprio così semplice. A proposito, Jython non ha GIL AFAIK e PyPy sta cercando di sbarazzarsene.

Risposte:


204

I greenlet forniscono concorrenza ma non parallelismo. La concorrenza è quando il codice può essere eseguito indipendentemente da altro codice. Il parallelismo è l'esecuzione simultanea di codice simultaneo. Il parallelismo è particolarmente utile quando c'è molto lavoro da fare nello spazio utente, e in genere è roba pesante per la CPU. La concorrenza è utile per risolvere i problemi, consentendo di programmare e gestire più facilmente diverse parti in parallelo.

I greenlet brillano davvero nella programmazione di rete in cui le interazioni con un socket possono avvenire indipendentemente dalle interazioni con altri socket. Questo è un classico esempio di concorrenza. Poiché ogni greenlet viene eseguito nel proprio contesto, è possibile continuare a utilizzare le API sincrone senza thread. Questo è positivo perché i thread sono molto costosi in termini di memoria virtuale e sovraccarico del kernel, quindi la concorrenza che puoi ottenere con i thread è significativamente inferiore. Inoltre, il threading in Python è più costoso e più limitato del solito a causa del GIL. Le alternative alla concorrenza sono in genere progetti come Twisted, libevent, libuv, node.js ecc., In cui tutto il codice condivide lo stesso contesto di esecuzione e registra i gestori di eventi.

È un'ottima idea utilizzare i greenlet (con il supporto di rete appropriato come tramite gevent) per scrivere un proxy, poiché la gestione delle richieste è in grado di essere eseguita in modo indipendente e deve essere scritta come tale.

I greenlet forniscono concorrenza per i motivi che ho indicato in precedenza. La concorrenza non è parallelismo. Nascondendo la registrazione degli eventi ed eseguendo la pianificazione per le chiamate che normalmente bloccherebbero il thread corrente, progetti come gevent espongono questa concorrenza senza richiedere la modifica di un'API asincrona ea costi notevolmente inferiori per il sistema.


1
Grazie, solo due piccole domande: 1) È possibile combinare questa soluzione con il multiprocessing per ottenere un throughput più elevato? 2) Non so ancora perché mai usare i thread? Possiamo considerarli come un'implementazione ingenua e di base della concorrenza nella libreria standard di Python?
Rsh

6
1) Sì, assolutamente. Non dovresti farlo prematuramente, ma a causa di tutta una serie di fattori oltre lo scopo di questa domanda, avere più processi che soddisfano le richieste ti darà un rendimento più elevato. 2) I thread del sistema operativo sono preventivamente programmati e completamente parallelizzati per impostazione predefinita. Sono i valori predefiniti in Python perché Python espone l'interfaccia di threading nativa e i thread sono il miglior denominatore comune supportato e più basso sia per il parallelismo che per la concorrenza nei moderni sistemi operativi.
Matt Joiner,

6
Devo dire che non dovresti nemmeno usare i greenlet fino a quando i thread non sono soddisfacenti (di solito ciò si verifica a causa del numero di connessioni simultanee che stai gestendo e o il conteggio dei thread o il GIL ti stanno dando dolore), e persino quindi solo se non ci sono altre opzioni disponibili per te. La libreria standard Python e la maggior parte delle librerie di terze parti si aspettano che la concorrenza venga raggiunta tramite thread, quindi potresti avere un comportamento strano se lo fornisci tramite greenlet.
Matt Joiner,

@MattJoiner Ho la seguente funzione che legge l'enorme file per calcolare la somma md5. come posso usare gevent in questo caso per leggere più velocemente 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 il

18

Prendendo la risposta di @ Max e aggiungendo una certa rilevanza ad essa per il ridimensionamento, puoi vedere la differenza. Ho raggiunto questo obiettivo modificando gli URL da compilare come segue:

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)

Ho dovuto abbandonare la versione multiprocesso in quanto è caduta prima di avere 500; ma a 10.000 iterazioni:

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

Quindi puoi vedere che c'è una differenza significativa nell'I / O usando gevent


4
è del tutto errato generare 60000 thread o processi nativi per completare il lavoro e questo test non mostra nulla (hai anche preso il timeout dalla chiamata gevent.joinall ()?). Provare a utilizzare un pool di thread di circa 50 fili, vedere la mia risposta: stackoverflow.com/a/51932442/34549
zzzeek

9

Correggendo la risposta di @TemporalBeing sopra, i greenlet non sono "più veloci" dei thread ed è una tecnica di programmazione errata generare spawn di 60000 thread per risolvere un problema di concorrenza, è invece appropriato un piccolo pool di thread. Ecco un confronto più ragionevole (dal mio post reddit in risposta alle persone che citano questo 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)

Ecco alcuni risultati:

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

il malinteso che tutti hanno sull'IO non bloccante con Python è la convinzione che l'interprete Python possa occuparsi del lavoro di recupero dei risultati dai socket su larga scala più velocemente di quanto le connessioni di rete stesse possano restituire IO. Anche se questo è certamente vero in alcuni casi, non lo è quasi tutte le volte che la gente pensa, perché l'interprete Python è molto, molto lento. Nel mio post di blog qui , illustrerò alcuni profili grafici che mostrano che per cose anche molto semplici, se hai a che fare con un accesso alla rete rapido e veloce a cose come database o server DNS, quei servizi possono tornare molto più velocemente del codice Python può occuparsi di molte migliaia di tali connessioni.


8

Questo è abbastanza interessante da analizzare. Ecco un codice per confrontare le prestazioni dei greenlet rispetto al pool di multiprocessing rispetto al 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()

ecco i risultati:

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

Penso che il greenlet sostenga che non è vincolato da GIL a differenza della libreria multithreading. Inoltre, il documento Greenlet afferma che è destinato alle operazioni di rete. Per un'operazione di rete intensiva, il cambio di thread va bene e puoi vedere che l'approccio multithreading è piuttosto veloce. Inoltre è sempre preferibile utilizzare le librerie ufficiali di Python; Ho provato a installare greenlet su Windows e ho riscontrato un problema di dipendenza DLL quindi ho eseguito questo test su un Linux VM. Cerca sempre di scrivere un codice con la speranza che funzioni su qualsiasi macchina.


25
Si noti che getsockbynamememorizza nella cache i risultati a livello di sistema operativo (almeno sulla mia macchina lo fa). Quando viene invocato su un DNS precedentemente sconosciuto o scaduto, eseguirà effettivamente una query di rete, che potrebbe richiedere del tempo. Quando viene invocato su un nome host che è stato recentemente risolto, restituirà la risposta molto più velocemente. Di conseguenza, la tua metodologia di misurazione è difettosa qui. Questo spiega i tuoi strani risultati - gevent non può davvero essere molto peggio del multithreading - entrambi non sono realmente paralleli a livello di VM.
KT.

1
@KT. questo è un punto eccellente. Dovresti eseguire quel test molte volte e prendere mezzi, modalità e mediane per ottenere una buona immagine. Si noti inoltre che i router memorizzano nella cache i percorsi di instradamento per i protocolli e laddove non memorizzano nella cache percorsi di instradamento, è possibile che si verifichino ritardi diversi rispetto al traffico di percorsi di instradamento DNS diversi. E server DNS pesantemente cache. Potrebbe essere meglio misurare il threading usando time.clock () dove vengono utilizzati i cicli cpu invece di essere effettuati dalla latenza sull'hardware di rete. Ciò potrebbe eliminare altri servizi OS che si intrufolano e aggiungono tempo dalle tue misurazioni.
DevPlayer,

Oh e puoi eseguire un flush DNS a livello di sistema operativo tra questi tre test, ma ciò ridurrebbe solo i dati falsi provenienti dalla cache DNS locale.
DevPlayer,

Sì. Esecuzione di questa versione pulita: ripulita paste.ubuntu.com/p/pg3KTzT2FG ottengo tempi praticamente identici ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
vedi

Penso che OSX stia eseguendo la memorizzazione nella cache DNS, ma su Linux non è una cosa "predefinita": stackoverflow.com/a/11021207/34549 , quindi sì, a bassi livelli di greenlet di concorrenza sono molto peggio a causa dell'overhead dell'interprete
zzzeek
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.