2014-10-02 6 views
16

나는 사이트의 도메인 이름을 열거하기 위해 사용되는 파이썬 프로그램을 작성 중이다. 예를 들어, 'a.google.com'.asyncio 라이브러리가이 I/O 바인딩 작업의 스레드보다 느린 이유는 무엇입니까?

첫째,이 작업을 수행 할 threading 모듈을 사용 :

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

나중에 내가 코 루틴이 빠르게 스레드보다 어떤 경우에 말했다, 책을 읽습니다. 그래서, 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 
''' 

getaddrinfoasyncio 버전이 너무 느린 무엇입니까? 코 루틴을 어떻게 든 오용하고 있습니까?

+1

나는 시스템에서 성능 차이를 거의 볼 수 없습니다. thread 버전은 20 초이고 asyncio 버전은 24입니다.'getaddr' 메소드에서 print 문을 제거해보십시오. 성능면에서 큰 차이가 있습니까? Printing은 GIL을 해제하므로 많은 스레드가 동시에이를 처리 할 수 ​​있지만 asyncio는 수행 할 수 없습니다. 시스템에서 인쇄 속도가 특히 느린 경우에는 속도 차이를 고려해야합니다. – dano

답변

26

첫째, 필자는 제 리눅스 컴퓨터에서 보는 성능 차이를 거의 재현 할 수 없습니다. 스레드 버전의 경우 약 20-25 초, asyncio 버전의 경우 24-34 초 사이를 계속 볼 수 있습니다.

이제 asyncio은 왜 느린 거죠? 이 문제에 기여하는 몇 가지 사항이 있습니다. 먼저 asyncio 버전이 순차적으로 인쇄되어야하지만 스레드 버전은 순차적으로 인쇄해야합니다. 인쇄는 I/O이므로 GIL이 발생하는 동안 GIL을 해제 할 수 있습니다. 즉, 잠재적으로 두 개 이상의 스레드가 정확히 같은 시간에 인쇄 할 수 있음을 의미합니다. 실제로는 자주 발생하지 않을 수 있으며, 성능면에서 그다지 큰 차이를 만들지는 못합니다.

둘째, 훨씬 더 중요한 것은, getaddrinfoasyncio 버전은 실제로 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) 

그것은이에 대한 기본 ThreadPoolExecutor을 사용하고, which only has five threads : 거의 아니다

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

이 유스 케이스에 대해 원하는 병렬 처리가 많이있다. 그것은 threading 버전 같은 더 동작하도록하려면 loop.set_default_executor를 통해 기본 실행기로 설정함으로써, ThreadPoolExecutor 1000 스레드를 사용해야 할 것 :

이제
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) 

,이 threading에 행동이 더 상당 할 것 하지만 현실은 입니다. 실제로 비동기 I/O를 사용하지 않고 있습니다. threading을 다른 API과 함께 사용하고 있습니다. 따라서 여기서 할 수있는 최선은 threading 예제와 동일한 성능입니다.

마지막으로, 당신은 정말 각각의 예에 해당하는 코드를 실행하지 않는 - asyncio 버전은 URL 목록에있는 모든 단일 항목에 대한 코 루틴을 산란하는 동안 threading 버전하는 queue.Queue를 공유하는 근로자의 풀을 사용하고 있습니다 . asyncio 버전을 asyncio.Queue과 coroutines 풀을 사용하도록 만들면 print 문을 제거하고 더 큰 기본 실행 프로그램을 만드는 것 외에도 두 버전 모두에서 본질적으로 동일한 성능을 얻습니다.여기에 새로운 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) 

그리고 출력 :하지만, 때문에 네트워크에 몇 가지 변화가

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

하는 것으로. 두 경우 모두 때로는 이보다 몇 초 느립니다.

+0

문제를 해결할 수 있도록 도와 주셔서 대단히 감사합니다. asyncio 버전이 비동기 I/O를 사용하지 않는다는 것을 이해합니다. 그런 다음 튤립 문제 160 (https://code.google.com/p)에서 문제를 검색했습니다./tulip/issues/detail? id = 160)도 언급됩니다. python2에서 gevent를 사용하거나 python3에서 aiodns를 사용하여 비동기 I/O를 사용합니다. –

+0

사실, coroutines 사용의 영향으로 asyncio가 훨씬 느립니다. 숫자가 없으므로 이것은 포스트가 아닌 단지 코멘트 일 뿐이지 만 두 스타일 모두로 작성된 간단한 http echo 서버로이를 확인할 수 있습니다. 파이썬 + 고성능 비동기 IO는 슬프게도 함께 작동하지 않습니다. Golang 또는 Java, Python + asyncio (IO 바인딩 만)와 비교할 때 파이썬은 대략 9 배 느립니다. ~ 32.000 Req/s 대 3.700 Req/s. 스레드 된 솔루션조차도 파이썬으로는 더 빠르며, 200 ~ 250 개 이상의 클라이언트를 사용하지 않는 한 더 빠릅니다. Asyncio는이 수의 클라이언트에서도 성능을 저하시킵니다. – Kr0e

+0

잘 모르겠습니다. 어쩌면 구현상의 버그 일 수도 있습니다. 안타깝게도 아직 공식적인 벤치 마크가 없으므로, 내 가정을 검증하거나 입증하는 것은 지금 당장은 매우 다르다. – Kr0e