2014-10-02 13 views
16

Tôi đang viết chương trình python được sử dụng để liệt kê tên miền của trang web.Ví dụ, 'a.google.com'.Tại sao thư viện asyncio chậm hơn các luồng cho thao tác I/O-bound này?

Đầu tiên, tôi đã sử dụng các mô-đun threading để làm điều này:

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

Sau đó, tôi đọc một cuốn sách mà nói trong một số trường hợp coroutines là nhanh hơn so với chủ đề. Vì vậy, tôi viết lại mã để sử dụng 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 
''' 

Tại sao là phiên bản asyncio của getaddrinfo như vậy là chậm? Tôi có sử dụng sai các coroutines không?

+1

Tôi không thấy sự khác biệt về hiệu suất trên hệ thống của mình. Phiên bản luồng là 20 giây, phiên bản asyncio là 24. Thử xóa câu lệnh in ra khỏi phương thức 'getaddr'. Điều đó có khác biệt lớn về hiệu suất không? In ấn giải phóng GIL, vì vậy nhiều luồng có thể thực hiện đồng thời, trong khi 'asyncio' không thể. Nếu việc in ấn đặc biệt chậm trên hệ thống của bạn, nó có thể giải thích sự khác biệt về tốc độ. – dano

Trả lời

26

Trước tiên, tôi không thể tạo lại sự khác biệt hiệu suất gần bằng với mức bạn thấy trên máy Linux của mình. Tôi liên tục thấy khoảng 20-25 giây đối với phiên bản luồng và giữa 24-34 giây đối với phiên bản asyncio.

Bây giờ, tại sao asyncio chậm hơn? Có một vài điều góp phần vào việc này. Đầu tiên, phiên bản asyncio phải in tuần tự, nhưng phiên bản luồng không. In ấn là I/O, vì vậy GIL có thể được phát hành trong khi nó đang xảy ra. Điều đó có nghĩa là có khả năng hai hoặc nhiều luồng có thể in cùng một lúc, mặc dù trong thực tế nó có thể không xảy ra thường xuyên, và có lẽ không tạo ra sự khác biệt nhiều về hiệu suất.

Thứ hai, và nhiều quan trọng hơn, phiên bản asyncio của getaddrinfo thực sự là 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) 

Đó là sử dụng mặc định ThreadPoolExecutor cho điều này, which only has five threads:

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

Đó là gần như không nhiều song song mà bạn muốn cho trường hợp sử dụng này. Để làm cho nó cư xử giống như phiên bản threading, bạn sẽ cần phải sử dụng một ThreadPoolExecutor với 1000 chủ đề, bằng cách thiết lập nó như là người thi hành mặc định qua 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) 

Bây giờ, điều này sẽ làm cho hành vi tương đương hơn để threading nhưng thực tế ở đây là bạn thực sự không sử dụng I/O không đồng bộ - bạn chỉ đang sử dụng threading với API khác. Vì vậy, tốt nhất bạn có thể làm ở đây là hiệu suất giống hệt với ví dụ threading. Cuối cùng, bạn không thực sự chạy mã tương đương trong mỗi ví dụ - phiên bản threading đang sử dụng một nhóm công nhân đang chia sẻ queue.Queue, trong khi phiên bản asyncio đang sinh ra một coroutine cho từng mục trong danh sách url . Nếu tôi thực hiện phiên bản asyncio để sử dụng asyncio.Queue và pool coroutines, ngoài việc xóa các câu lệnh in và thực hiện một trình thực thi mặc định lớn hơn, tôi nhận được hiệu suất cơ bản giống hệt nhau với cả hai phiên bản.Đây là asyncio mã mới:

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) 

Và Sản lượng mỗi:

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

Lưu ý rằng có một số thay đổi do mạng, mặc dù. Cả hai người trong số họ đôi khi sẽ chậm hơn vài giây so với điều này.

+0

Cảm ơn bạn rất nhiều vì đã giúp tôi giải quyết vấn đề. Nó làm cho tôi hiểu phiên bản asyncio của tôi không sử dụng I/O không đồng bộ. Sau đó, tôi đã tìm kiếm sự cố về vấn đề hoa tulip 160 (https://code.google.com/p/tulip/issues/detail? id = 160) cũng được đề cập. Tôi sẽ sử dụng gevent trong python2 hoặc sử dụng aiodns trong python3 để sử dụng I/O không đồng bộ. –

+0

Thực ra, asyncio chậm hơn nhiều do tác động của việc sử dụng coroutines. Tôi không có số, vì vậy đây chỉ là một bình luận, thay vì một bài viết, nhưng bạn có thể xác minh điều này với một máy chủ echo http đơn giản được viết bằng cả hai kiểu. Python + hiệu suất cao async IO không làm việc cùng nhau, thật đáng buồn. So với Golang hoặc Java, Python + asyncio (chỉ IO bị ràng buộc), python chậm hơn khoảng 9 lần. ~ 32.000 Req/s so với 3.700 Req/s. Ngay cả các giải pháp luồng là nhanh hơn với python, miễn là bạn không sử dụng nhiều hơn nói 200 ~ 250 khách hàng. Asyncio giảm hiệu suất cũng ở số lượng khách hàng này. – Kr0e

+0

Tôi không chắc chắn, có lẽ nó cũng là một lỗi trong việc thực hiện. Đáng buồn thay, không có điểm chuẩn chính thức nào, vì vậy việc xác nhận hoặc chứng minh giả định của tôi là hoàn toàn khác nhau ngay bây giờ ... – Kr0e

Các vấn đề liên quan