Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the asyncio library slower than threads for this I/O-bound operation?

Tags:

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?

like image 733
oi__io Avatar asked Oct 02 '14 03:10

oi__io


People also ask

Is Asyncio faster than threads?

One of the cool advantages of asyncio is that it scales far better than threading . Each task takes far fewer resources and less time to create than a thread, so creating and running more of them works well. This example just creates a separate task for each site to download, which works out quite well.

Is async faster than multithreading Python?

Tasks + async / await are faster in this case than a pure multi threaded code. It's the simplicity which makes async / await so appealing.

Is Asyncio multithreading or multiprocessing?

Asyncio allowing for IO bound traffic, but multiprocessing allowing multiple event loops and threads on multiple cores.


1 Answers

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, the asyncio 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 of getaddrinfo is actually 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) 

It's using the default ThreadPoolExecutor for this, which only has five threads:

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

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 a ThreadPoolExecutor with 1000 threads, by setting it as the default executor via 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) 

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 using threading with a different API. So the best you can do here is identical performance to the threading example.

Finally, you're not really running equivalent code in each example - the threading version is using a pool of workers, which are sharing a queue.Queue, while the asyncio version is spawning a coroutine for every single item in the url list. If I make the asyncio version to use a asyncio.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 new asyncio code:

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) 

And Output of each:

dan@dandesk:~$ python3 threaded_example.py 20.409344911575317 dan@dandesk:~$ python3 asyncio_example.py 20.39924192428589 

Note that there is some variability due to the network, though. Both of them will sometimes be a few seconds slower than this.

like image 61
dano Avatar answered Oct 26 '22 09:10

dano