2014-10-02 1 views
16

Sto scrivendo un programma python utilizzato per enumerare il nome di dominio di un sito.Ad esempio, 'a.google.com'.Perché la libreria asyncio è più lenta dei thread per questa operazione legata all'I/O?

In primo luogo, ho usato il modulo threading per fare questo:

import string 
import time 
import socket 
import threading 
from threading import Thread 
from queue import Queue 

''' 
enumerate a site's domain name like this: 
1-9 a-z + .google.com 
1.google.com 
2.google.com 
. 
. 
1a.google.com 
. 
. 
zz.google.com 

''' 

start = time.time() 
def create_host(char): 
    ''' 
    if char is '1-9a-z' 
    create char like'1,2,3,...,zz' 
    ''' 
    for i in char: 
     yield i 
    for i in create_host(char): 
     if len(i)>1: 
      return False 
     for c in char: 
      yield c + i 


char = string.digits + string.ascii_lowercase 
site = '.google.com' 


def getaddr(): 
    while True: 
     url = q.get() 
     try: 
      res = socket.getaddrinfo(url,80) 
      print(url + ":" + res[0][4][0]) 
     except: 
      pass 
     q.task_done() 

NUM=1000 #thread's num 
q=Queue() 

for i in range(NUM): 
    t = Thread(target=getaddr) 
    t.setDaemon(True) 
    t.start() 

for host in create_host(char): 
    q.put(host+site) 
q.join() 

end = time.time() 

print(end-start) 

''' 
used time: 
9.448670148849487 
''' 

Più tardi, ho letto un libro che ha detto, in alcuni casi coroutine sono più veloce di discussioni. Così, ho riscritto il codice per utilizzare asyncio:

import asyncio 
import string 
import time 


start = time.time() 
def create_host(char): 
    for i in char: 
     yield i 
    for i in create_host(char): 
     if len(i)>1: 
      return False 
     for c in char: 
      yield c + i 


char = string.digits + string.ascii_lowercase 
site = '.google.com' 

@asyncio.coroutine 
def getaddr(loop, url): 
    try: 
     res = yield from loop.getaddrinfo(url,80) 
     print(url + ':' + res[0][4][0]) 
    except: 
     pass 

loop = asyncio.get_event_loop() 
coroutines = asyncio.wait([getaddr(loop, i+site) for i in create_host(char)]) 
loop.run_until_complete(coroutines) 

end = time.time() 

print(end-start) 


''' 
time 
120.42313003540039 
''' 

Perché è la versione asyncio di getaddrinfo è così lento? Sto abusando delle coroutine in qualche modo?

+1

Non vedo quasi nessuna differenza di prestazioni sul mio sistema. La versione filettata era di 20 secondi, la versione di asyncio era 24. Prova a rimuovere la dichiarazione di stampa dal metodo 'getaddr'. Ciò rende le prestazioni molto diverse? La stampa rilascia GIL, quindi molti thread possono farlo contemporaneamente, mentre 'asyncio' non può. Se la stampa è particolarmente lenta sul sistema, potrebbe tenere conto della differenza di velocità. – dano

risposta

26

In primo luogo, non riesco a riprodurre una differenza di prestazioni grande quasi quanto quella che si vede sul mio computer Linux. Sto costantemente vedendo circa 20-25 secondi per la versione filettata, e tra 24-34 secondi per la versione asyncio.

Ora, perché è asyncio più lento? Ci sono alcune cose che contribuiscono a questo. Innanzitutto, la versione asyncio deve essere stampata in modo sequenziale, ma la versione con threading no. La stampa è I/O, quindi il GIL può essere rilasciato mentre sta accadendo. Ciò significa che potenzialmente due o più thread possono stampare allo stesso tempo, anche se in pratica potrebbe non accadere spesso, e probabilmente non fa molta differenza nelle prestazioni.

In secondo luogo, e molto più importante, la versione asyncio di getaddrinfo è in realtà just calling socket.getaddrinfo in a ThreadPoolExecutor:

def getaddrinfo(self, host, port, *, 
       family=0, type=0, proto=0, flags=0): 
    if self._debug: 
     return self.run_in_executor(None, self._getaddrinfo_debug, 
            host, port, family, type, proto, flags) 
    else: 
     return self.run_in_executor(None, socket.getaddrinfo, 
            host, port, family, type, proto, flags) 

che sta utilizzando il default ThreadPoolExecutor per questo, which only has five threads:

# Argument for default thread pool executor creation. 
_MAX_WORKERS = 5 

Questo non è quasi molto parallelismo che vuoi per questo caso d'uso. Per rendere comportarsi più come la versione threading, avresti bisogno di utilizzare un ThreadPoolExecutor con 1.000 discussioni, impostandola come l'esecutore di default tramite loop.set_default_executor:

loop = asyncio.get_event_loop() 
loop.set_default_executor(ThreadPoolExecutor(1000)) 
coroutines = asyncio.wait([getaddr(loop, i+site) for i in create_host(char)]) 
loop.run_until_complete(coroutines) 

Ora, questo renderà il comportamento più equivalente a threading , ma la realtà qui è in realtà non stai usando I/O asincrono - stai usando solo threading con un'API diversa. Quindi il meglio che puoi fare qui è la stessa prestazione per l'esempio threading.

Infine, non sei davvero l'esecuzione di codice equivalente in ogni esempio - la versione threading sta usando una riserva di lavoratori, che condividono un queue.Queue, mentre la versione asyncio sta generando un coroutine per ogni singolo elemento della lista url . Se creo la versione asyncio per utilizzare un asyncio.Queue e un pool di coroutine, oltre alla rimozione delle istruzioni di stampa e all'esecuzione di un esecutore predefinito di dimensioni maggiori, ottengo prestazioni sostanzialmente identiche con entrambe le versioni.Ecco il nuovo codice asyncio:

import asyncio 
import string 
import time 
from concurrent.futures import ThreadPoolExecutor 

start = time.time() 
def create_host(char): 
    for i in char: 
     yield i 
    for i in create_host(char): 
     if len(i)>1: 
      return False 
     for c in char: 
      yield c + i 


char = string.digits + string.ascii_lowercase 
site = '.google.com' 

@asyncio.coroutine 
def getaddr(loop, q): 
    while True: 
     url = yield from q.get() 
     if not url: 
      break 
     try: 
      res = yield from loop.getaddrinfo(url,80) 
     except: 
      pass 

@asyncio.coroutine 
def load_q(loop, q): 
    for host in create_host(char): 
     yield from q.put(host+site) 
    for _ in range(NUM): 
     yield from q.put(None) 

NUM = 1000 
q = asyncio.Queue() 

loop = asyncio.get_event_loop() 
loop.set_default_executor(ThreadPoolExecutor(NUM)) 
coros = [asyncio.async(getaddr(loop, q)) for i in range(NUM)] 
loop.run_until_complete(load_q(loop, q)) 
loop.run_until_complete(asyncio.wait(coros)) 

end = time.time() 

print(end-start) 

e di uscita di ogni:

[email protected]:~$ python3 threaded_example.py 
20.409344911575317 
[email protected]:~$ python3 asyncio_example.py 
20.39924192428589 

Si noti che v'è una certa variabilità dovuta alla rete, però. Entrambi a volte saranno pochi secondi più lenti di questo.

+0

Grazie mille per avermi aiutato a risolvere il problema. Mi fa capire che la mia versione di asyncio non utilizza l'I/O asincrono. Quindi ho cercato e trovato il problema sui problemi tulip 160 (https://code.google.com/pp/tulip/issues/detail? id = 160) Sono anche menzionati. Userò gevent in python2 o userò iodns in python3 per usare l'I/O asincrono. –

+0

In realtà, l'asyncio è molto più lento a causa dell'elevato impatto dell'utilizzo di coroutine. Non ho numeri, quindi questo è solo un commento, invece di un post, ma puoi verificarlo con un semplice server http echo scritto in entrambi gli stili. Python + I/O asincrono ad alte prestazioni non funzionano insieme, purtroppo. Rispetto a Golang o Java, Python + asyncio (solo IO legato), python è all'incirca 9 volte più lento. ~ 32.000 Req/s contro 3.700 Req/s. Anche la soluzione con thread è più veloce con python, a patto che non si usi più di 200 ~ 250 client. Asyncio riduce le prestazioni anche a questo numero di clienti. – Kr0e

+0

Non sono sicuro, forse è anche un bug nell'implementazione. Purtroppo, non ci sono ancora benchmark ufficiali, quindi validare o dimostrare che la mia ipotesi è alquanto diversa in questo momento ... – Kr0e