I'm writing a python program used to enumerate a site's domain name.For example,'a.google.com'.
First, I used the threading
module to do this:
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
'''
Later, I read a book which said in some cases coroutines are faster than threads. So, I rewrote the code to use 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
'''
Why is the asyncio
version of getaddrinfo
is so slow? Am I misusing the coroutines somehow?
First, I can't reproduce a performance difference nearly as large as the one you're seeing on my Linux machine. I'm consistently seeing about 20-25 seconds for the threaded version, and between 24-34 seconds for the
asyncio
version.Now, why is
asyncio
slower? There are a few things that contribute to this. First, theasyncio
version has to print sequentially, but the threaded version doesn't. Printing is I/O, so the GIL can be released while it's happening. That means potentially two or more threads can print at the exact same time, though in practice it may not happen often, and probably doesn't make all that much difference in performance.Second, and much more importantly, the
asyncio
version ofgetaddrinfo
is actually just callingsocket.getaddrinfo
in aThreadPoolExecutor
:It's using the default
ThreadPoolExecutor
for this, which only has five threads:That's not nearly as much parallelism you want for this use-case. To make it behave more like the
threading
version, you'd need to use aThreadPoolExecutor
with 1000 threads, by setting it as the default executor vialoop.set_default_executor
:Now, this will make the behavior more equivalent to
threading
, but the reality here is you're really not using asynchronous I/O - you're just usingthreading
with a different API. So the best you can do here is identical performance to thethreading
example.Finally, you're not really running equivalent code in each example - the
threading
version is using a pool of workers, which are sharing aqueue.Queue
, while theasyncio
version is spawning a coroutine for every single item in the url list. If I make theasyncio
version to use aasyncio.Queue
and pool of coroutines, in addition to the removing the print statements and making a larger default executor, I get essentially identical performance with both versions. Here's the newasyncio
code:And Output of each:
Note that there is some variability due to the network, though. Both of them will sometimes be a few seconds slower than this.