When and when not to multithread

At the end of my last post on Python multithreading, I said my example was not the best. Let me expand some more on this.

While testing code in the previous post, I noticed that certain code was slower when multiple threads were running. Also these threads are not tied to a CPU. If we were talking about a bigger applications in which we wanted to ensure multiple threads were on different CPUs, you are in fact looking for multiprocessing.

Consider the following code. It simple counts to 99 999, doubles the number, then prints this to the screen. At first I’ll do this as a single thread app then multithread and time them.

Single-thread

#!/usr/bin/python

for i in range (100000):
    i *= 2
    print i

Multi-thread

#!/usr/bin/python

import threading
lock = threading.Lock()

def thread_test(i):
    i *= 2
    with lock:
        print i

threads = []
for i in range (100000):
    t = threading.Thread(target = thread_test, args = (i,))
    threads.append(t)
    t.start()

I’ll now time and run the command. I’ll run each command three times and take the average of all three:

time ./single.py

The single thread is able to do this in 0.411 seconds, while the multithreaded app takes a full 16.409 seconds.

Now I’ll do a test in which multithreading will make a big difference. I have a list of 500 random urls. I want to log into each, then get them to display the page contents. Not all urls respond, and I’ve also given a three second timeout to fetching any page.

The single thread app is coded like so:

#!/usr/bin/python

import urllib2

with open("urls.txt", "r") as f:
    urls = f.readlines()

for url in urls:
    request = urllib2.Request(url)
    try:
        response = urllib2.urlopen(request, timeout = 3)
        page = response.read()
        print page
    except:
        print "Unable to download"

This takes a full 11 minutes and 40 seconds to fully run.

Converted to multithread:

#!/usr/bin/python
 
import urllib2
import threading

lock = threading.Lock()

def thread_test(url):
    try:
        response = urllib2.urlopen(url, timeout = 3)
        page = response.read()
        with lock:
            print page
    except:
        with lock:
            print "Unable to download"

with open("urls.txt", "r") as f:
   urls = f.readlines()
threads = []

for url in urls:
    request = urllib2.Request(url)
    t = threading.Thread(target = thread_test, args = (request,))
    threads.append(t)
    t.start()

This time the average over 3 runs is only 1 minute and 40 seconds.

I am however still locking output to the screen. This may be bad practice, but let’s assume I don’t really care about visible output. Maybe I just want to throw some commands somewhere, or something simple like ping. If I didn’t lock before printing, how quickly could this actually run?

#!/usr/bin/python
 
import urllib2
import threading

lock = threading.Lock()

def thread_test(url):
    try:
        response = urllib2.urlopen(url, timeout = 3)
        page = response.read()
    except:
        pass

with open("urls.txt", "r") as f:
   urls = f.readlines()
threads = []

for url in urls:
    request = urllib2.Request(url)
    t = threading.Thread(target = thread_test, args = (request,))
    threads.append(t)
    t.start()

This completes in 1 minute and 13 seconds. Not as much as I hoped for. But it does mean one thing. Python is not running ALL the threads at exactly the same time. If that was the case, the max run time would be just over three seconds as that’s what the timeout is.

I’ll load up Wireshark and run the test again. I should see how many threads are sending HTTP GETs at the same time. When I start the threads, I can see 26 threads all starting within a second of each other. Only a full 7 seconds later do others start:
Screen Shot 2014-12-14 at 13.39.51

After that, I see more threads being added as others end. The timing seems random later as each page has a different response time.
Screen Shot 2014-12-14 at 13.41.20

This seems to be an OS imposed limit.

Conclusions

  • Multithreading in Python has certain benefits only in specific cases.
  • Mainly if you are requesting data from many different sources. No need to query them one at a time.
  • Python’s initial single thread is rather efficient all by itself.

I’m certainly not going to rewrite all my current code to use multithreading. Going back to my original OSPF checker, it certainly would be good to check pull information off multiple devices at the same time, but the rest of the app I’d still keep as a single thread.

© 2009-2020 Darren O'Connor All Rights Reserved -- Copyright notice by Blog Copyright