Greenlet vs. Tópicos

141

Eu sou novo em gevents e greenlets. Encontrei uma boa documentação sobre como trabalhar com eles, mas nenhuma me deu justificativa sobre como e quando devo usar os greenlets!

  • No que eles são realmente bons?
  • É uma boa ideia usá-los em um servidor proxy ou não?
  • Por que não tópicos?

O que não tenho certeza é como eles podem nos fornecer simultaneidade se forem basicamente co-rotinas.

Rsh
fonte
1
@Imran Trata-se de greenthreads em Java. Minha pergunta é sobre greenlet em Python. Estou esquecendo de algo ?
Rsh
Além disso, os threads em python não são realmente concorrentes devido ao bloqueio global de intérpretes. Portanto, tudo se resumia à comparação das despesas gerais das duas soluções. Embora eu entenda que existem várias implementações de python, isso pode não se aplicar a todas elas.
didierc
3
@didierc O CPython (e o PyPy a partir de agora) não interpretará o código Python (byte) em paralelo (ou seja, realmente fisicamente ao mesmo tempo em dois núcleos distintos da CPU). No entanto, nem tudo o que um programa Python faz está sob o GIL (exemplos comuns são syscalls, incluindo funções de E / S e C que deliberadamente liberam o GIL), e a threading.Threadé realmente um encadeamento do SO com todas as ramificações. Portanto, não é tão simples assim. A propósito, Jython não tem GIL AFAIK e PyPy tentando se livrar dele também.

Respostas:

204

Os greenlets fornecem simultaneidade, mas não paralelismo. Simultaneidade é quando o código pode ser executado independentemente de outro código. Paralelismo é a execução de código simultâneo simultaneamente. O paralelismo é particularmente útil quando há muito trabalho a ser feito no espaço do usuário, e isso geralmente envolve coisas pesadas na CPU. A simultaneidade é útil para desmembrar problemas, permitindo que diferentes partes sejam agendadas e gerenciadas mais facilmente em paralelo.

Os greenlets realmente brilham na programação de rede em que as interações com um soquete podem ocorrer independentemente das interações com outros soquetes. Este é um exemplo clássico de simultaneidade. Como cada greenlet é executado em seu próprio contexto, você pode continuar usando APIs síncronas sem segmentação. Isso é bom porque os threads são muito caros em termos de memória virtual e sobrecarga do kernel; portanto, a simultaneidade que você pode obter com os threads é significativamente menor. Além disso, a segmentação em Python é mais cara e mais limitada do que o normal devido ao GIL. Alternativas à simultaneidade são geralmente projetos como Twisted, libevent, libuv, node.js etc., em que todo o seu código compartilha o mesmo contexto de execução e registra manipuladores de eventos.

É uma excelente ideia usar greenlets (com suporte de rede apropriado, como por meio de gevent) para escrever um proxy, pois o processamento de solicitações é capaz de executar independentemente e deve ser escrito como tal.

Os greenlets proporcionam simultaneidade pelas razões que mencionei anteriormente. Concorrência não é paralelismo. Ao ocultar o registro de eventos e executar o agendamento para você em chamadas que normalmente bloqueariam o encadeamento atual, projetos como gevent expõem essa simultaneidade sem exigir a alteração de uma API assíncrona e a um custo significativamente menor para o seu sistema.

Matt Joiner
fonte
1
Obrigado, apenas duas pequenas perguntas: 1) É possível combinar esta solução com o multiprocessamento para obter maior produtividade? 2) Ainda não sei por que usar threads? Podemos considerá-los uma implementação ingênua e básica de simultaneidade na biblioteca padrão do python?
Rsh
6
1) Sim, absolutamente. Você não deve fazer isso prematuramente, mas devido a vários fatores além do escopo desta pergunta, ter vários processos para atender a solicitações fornecerá maior taxa de transferência. 2) Os threads do sistema operacional são agendados preventivamente e totalmente paralelizados por padrão. Eles são o padrão no Python porque o Python expõe a interface de encadeamento nativa, e os threads são o denominador comum mais baixo e com melhor suporte para paralelismo e simultaneidade nos sistemas operacionais modernos.
Matt Joiner
6
Devo mencionar que você nem deve usar greenlets até que os threads não sejam satisfatórios (geralmente isso ocorre devido ao número de conexões simultâneas que você está manipulando, e a contagem de threads ou o GIL estão lhe causando pesar) e até somente se não houver outra opção disponível para você. A biblioteca padrão do Python e a maioria das bibliotecas de terceiros esperam que a simultaneidade seja alcançada por meio de encadeamentos; portanto, você pode obter um comportamento estranho se fornecer isso por meio de greenlets.
precisa
@MattJoiner Eu tenho a função abaixo, que lê o arquivo enorme para calcular a soma MD5. como posso usar o gevent neste caso para ler mais 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 a resposta do @ Max e adicionando alguma relevância para o dimensionamento, você pode ver a diferença. Consegui isso alterando os URLs a serem preenchidos da seguinte maneira:

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)

Eu tive que abandonar a versão multiprocesso, pois ela caía antes dos 500; mas em 10.000 iterações:

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

Então você pode ver que há alguma diferença significativa na E / S usando o gevent

TemporalBeing
fonte
4
é totalmente incorreto gerar 60000 encadeamentos ou processos nativos para concluir o trabalho e esse teste não mostra nada (você também tirou o tempo limite da chamada gevent.joinall ()?). Tente usar um pool de threads de cerca de 50 tópicos, veja a minha resposta: stackoverflow.com/a/51932442/34549
zzzeek
9

Corrigindo a resposta do @TemporalBeing acima, os greenlets não são "mais rápidos" que os threads e é uma técnica de programação incorreta gerar 60000 threads para resolver um problema de simultaneidade, um pequeno conjunto de threads é apropriado. Aqui está uma comparação mais razoável (do meu post no reddit em resposta a pessoas que citam este post no 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)

Aqui estão alguns 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

o mal-entendido que todos têm sobre E / S não bloqueadoras com Python é a crença de que o intérprete Python pode prestar atenção ao trabalho de recuperar resultados de soquetes em larga escala mais rapidamente do que as próprias conexões de rede podem devolver E / S. Embora isso certamente seja verdade em alguns casos, não é quase tão freqüentemente quanto as pessoas pensam, porque o intérprete Python é muito, muito lento. No meu blog aqui , ilustro alguns perfis gráficos que mostram que, mesmo para coisas muito simples, se você estiver lidando com acesso rápido e nítido à rede a coisas como bancos de dados ou servidores DNS, esses serviços poderão retornar muito mais rapidamente que o código Python pode atender a muitos milhares dessas conexões.

zzzeek
fonte
8

Isso é interessante o suficiente para analisar. Aqui está um código para comparar o desempenho de greenlets versus pool de multiprocessamento 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()

aqui estão os resultados:

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

Penso que o greenlet alega que não está vinculado pelo GIL, ao contrário da biblioteca multithreading. Além disso, o documento Greenlet diz que se destina a operações de rede. Para uma operação intensiva em rede, a troca de threads é boa e você pode ver que a abordagem de multithreading é bastante rápida. Também é sempre preferível usar as bibliotecas oficiais do python; Eu tentei instalar o greenlet no windows e encontrou um problema de dependência da dll, então eu executei este teste em um linux vm. Sempre tente escrever um código com a esperança de que seja executado em qualquer máquina.

max
fonte
25
Observe que getsockbynameos resultados são armazenados em cache no nível do sistema operacional (pelo menos na minha máquina). Quando invocado em um DNS anteriormente desconhecido ou expirado, ele efetivamente realiza uma consulta de rede, o que pode levar algum tempo. Quando invocado em um nome de host que foi resolvido recentemente, ele retornará a resposta muito mais rapidamente. Consequentemente, sua metodologia de medição é falha aqui. Isso explica seus resultados estranhos - o gevent não pode ser muito pior do que o multithreading - ambos não são realmente paralelos no nível da VM.
KT.
1
@KT. Esse é um ponto excelente. Você precisaria executar esse teste várias vezes e usar meios, modos e medianas para obter uma boa imagem. Observe também que os roteadores armazenam em cache os caminhos de rota para os protocolos e onde eles não armazenam em cache os caminhos de rota, você pode obter um atraso diferente do tráfego de caminho de rota de DNS diferente. E servidores DNS fortemente cache. Talvez seja melhor medir o encadeamento usando time.clock (), onde os ciclos da CPU são usados ​​em vez de serem afetados pela latência no hardware da rede. Isso poderia eliminar outros serviços do sistema operacional e aumentar o tempo de suas medições.
DevPlayer
Ah, e você pode executar uma descarga de DNS no nível do SO entre esses três testes, mas novamente isso reduziria apenas dados falsos do cache local de DNS.
DevPlayer 13/10/16
Sim. Executando esta versão limpa: paste.ubuntu.com/p/pg3KTzT2FG Eu recebo tempos praticamente idênticos ...using_gevent() 421.442985535ms using_multiprocessing() 394.540071487ms using_multithreading() 402.48298645ms
sehe
Acho que o OSX está fazendo cache de DNS, mas no Linux não é uma coisa "padrão": stackoverflow.com/a/11021207/34549 , então sim, em baixos níveis de greenlets de simultaneidade são muito piores devido à sobrecarga do interpretador
zzzeek