<jupyter_start><jupyter_text>Multithreading and Multiprocessing* Published a blog post here: https://medium.com/@bfortuner/python-multithreading-vs-multiprocessing-73072ce5600b<jupyter_code>from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import numpy as np
import time
import matplotlib.pyplot as plt
import glob
from PIL import Image
import random
import string
%matplotlib inline
MULTITHREADING_TITLE="Multithreading"
MULTIPROCESSING_TITLE="Multiprocessing"

def visualize_runtimes(results, title):
    start,stop = np.array(results).T
    plt.barh(range(len(start)),stop-start,left=start)
    plt.grid(axis='x')
    plt.ylabel("Tasks")
    plt.xlabel("Seconds")
    plt.title(title)
    return stop[-1]-start[0]
def multithreading(func, args, workers):
    begin_time = time.time()
    with ThreadPoolExecutor(max_workers=workers) as executor:
        res = executor.map(func, args, [begin_time for i in range(len(args))])
    return list(res)
        
def multiprocessing(func, args, workers):
    begin_time = time.time()
    with ProcessPoolExecutor(max_workers=workers) as executor:
        res = executor.map(func, args, [begin_time for i in range(len(args))])
    return list(res)<jupyter_output><empty_output><jupyter_text>API Calls<jupyter_code>from urllib.request import urlopen

def download(url, base):
    start = time.time() - base
    try:
        resp = urlopen(url)
    except Exception as e:
        print ('ERROR: %s' % e)
    stop = time.time() - base
    return start,stop
N = 16
URL = 'http://scholar.princeton.edu/sites/default/files/oversize_pdf_test_0.pdf'
urls = [URL for i in range(N)]<jupyter_output><empty_output><jupyter_text>Serial<jupyter_code>%timeit -n 1 [download(url, 1) for url in urls]<jupyter_output>1 loop, best of 3: 6.36 s per loop<jupyter_text>Multithreading<jupyter_code>visualize_runtimes(multithreading(download, urls, 1), "Single Thread")
visualize_runtimes(multithreading(download, urls, 2),MULTITHREADING_TITLE)
visualize_runtimes(multithreading(download, urls, 4),MULTITHREADING_TITLE)<jupyter_output><empty_output><jupyter_text>Multiprocessing<jupyter_code>visualize_runtimes(multiprocessing(download, urls, 1), "Single Process")
visualize_runtimes(multiprocessing(download, urls, 2), MULTIPROCESSING_TITLE)
visualize_runtimes(multiprocessing(download, urls, 4), MULTIPROCESSING_TITLE)<jupyter_output><empty_output><jupyter_text>IO Heavy<jupyter_code>def io_heavy(text,base):
    start = time.time() - base
    f = open('output.txt', 'wt', encoding='utf-8')
    f.write(text)
    f.close()
    stop = time.time() - base
    return start,stop
N=12
TEXT = ''.join(random.choice(string.ascii_lowercase) for i in range(10**7*5))<jupyter_output><empty_output><jupyter_text>Serial<jupyter_code>%timeit -n 1 [io_heavy(TEXT,1) for i in range(N)]<jupyter_output>1 loop, best of 3: 1.37 s per loop<jupyter_text>MultithreadingShould see good benefit from this<jupyter_code>visualize_runtimes(multithreading(io_heavy, [TEXT for i in range(N)], 1),"Single Thread")
visualize_runtimes(multithreading(io_heavy, [TEXT for i in range(N)], 2),MULTITHREADING_TITLE)
visualize_runtimes(multithreading(io_heavy, [TEXT for i in range(N)], 4),MULTITHREADING_TITLE)<jupyter_output><empty_output><jupyter_text>MultiprocessingShould see good benefit from this<jupyter_code>visualize_runtimes(multiprocessing(io_heavy, [TEXT for i in range(N)], 1),"Single Process")
visualize_runtimes(multiprocessing(io_heavy, [TEXT for i in range(N)], 2),MULTIPROCESSING_TITLE)
visualize_runtimes(multiprocessing(io_heavy, [TEXT for i in range(N)], 4),MULTIPROCESSING_TITLE)<jupyter_output><empty_output><jupyter_text>Numpy Addition<jupyter_code>#Does not use parallel processing by default
#But will see speedups if multiprocessing used
#Because numpy sidesteps python's GIL
def addition(i, base):
    start = time.time() - base
    res = a + b
    stop = time.time() - base
    return start,stop
DIMS = 20000
N = 20
DIMS_ARR = [DIMS for i in range(N)]
a = np.random.rand(DIMS,DIMS)
b = np.random.rand(DIMS,DIMS)<jupyter_output><empty_output><jupyter_text>Serial<jupyter_code>%timeit -n 1 [addition(i, time.time()) for i in range(N)]<jupyter_output>1 loop, best of 3: 14.9 s per loop<jupyter_text>MultithreadingSome benefit for numpy addition (operation avoids GIL, but not parallel by default)<jupyter_code>visualize_runtimes(multithreading(addition, [i for i in range(N)], 1),"Single Thread")
visualize_runtimes(multithreading(addition, [i for i in range(N)], 2),MULTITHREADING_TITLE)
visualize_runtimes(multithreading(addition, [i for i in range(N)], 4),MULTITHREADING_TITLE)<jupyter_output><empty_output><jupyter_text>MultiprocessingSome benefit for numpy addition (operation avoids GIL, but not parallel by default)<jupyter_code>visualize_runtimes(multiprocessing(addition, [i for i in range(N)], 1),"Single Process")
visualize_runtimes(multiprocessing(addition, [i for i in range(N)], 1),MULTIPROCESSING_TITLE)
visualize_runtimes(multiprocessing(addition, [i for i in range(N)], 1),MULTIPROCESSING_TITLE)<jupyter_output><empty_output><jupyter_text>Dot Product<jupyter_code>#Automatic parallel processing built works out of the box
#Depending on BLAS impl, MKL (default with anaconda3) does
#Should NOT see speedups with multithreading/processing
def dot_product(i, base):
    start = time.time() - base
    res = np.dot(a,b)
    stop = time.time() - base
    return start,stop

DIMS = 3000
N = 10
DIMS_ARR = [DIMS for i in range(N)]
a = np.random.rand(DIMS,DIMS)
b = np.random.rand(DIMS,DIMS)<jupyter_output><empty_output><jupyter_text>Serial<jupyter_code>%timeit -n 1 [dot_product(i, time.time()) for i in range(N)]<jupyter_output>1 loop, best of 3: 2.87 s per loop<jupyter_text>MultithreadingNo benefit on dot product (since already parallel)<jupyter_code>visualize_runtimes(multithreading(dot_product, [i for i in range(N)], 1),"Single Thread")
visualize_runtimes(multithreading(dot_product, [i for i in range(N)], 2),MULTITHREADING_TITLE)
visualize_runtimes(multithreading(dot_product, [i for i in range(N)], 4),MULTITHREADING_TITLE)<jupyter_output><empty_output><jupyter_text>MultiprocessingNo benefit on dot product (since already parallel)<jupyter_code>visualize_runtimes(multiprocessing(dot_product, [i for i in range(N)], 1),"Single Process")
visualize_runtimes(multiprocessing(dot_product, [i for i in range(N)], 2),MULTIPROCESSING_TITLE)
visualize_runtimes(multiprocessing(dot_product, [i for i in range(N)], 4),MULTIPROCESSING_TITLE)<jupyter_output><empty_output><jupyter_text>CPU Intensive<jupyter_code>def cpu_heavy(n,base):
    start = time.time() - base
    count = 0
    for i in range(n):
        count += i
    stop = time.time() - base
    return start,stop
N = 10**7
ITERS = 10<jupyter_output><empty_output><jupyter_text>Serial<jupyter_code>%timeit -n 1 [cpu_heavy(N, time.time()) for i in range(ITERS)]<jupyter_output>1 loop, best of 3: 4.22 s per loop<jupyter_text>MultithreadingNo benefit on CPU-intensive tasks<jupyter_code>visualize_runtimes(multithreading(cpu_heavy, [N for i in range(ITERS)], 1),"Single Thread")
visualize_runtimes(multithreading(cpu_heavy, [N for i in range(ITERS)], 2),MULTITHREADING_TITLE)
visualize_runtimes(multithreading(cpu_heavy, [N for i in range(ITERS)], 4),MULTITHREADING_TITLE)<jupyter_output><empty_output><jupyter_text>MultiprocessingShows benefits on CPU-intensive tasks<jupyter_code>visualize_runtimes(multiprocessing(cpu_heavy, [N for i in range(ITERS)], 1),"Single Process")
visualize_runtimes(multiprocessing(cpu_heavy, [N for i in range(ITERS)], 2),MULTIPROCESSING_TITLE)
visualize_runtimes(multiprocessing(cpu_heavy, [N for i in range(ITERS)], 4),MULTIPROCESSING_TITLE)<jupyter_output><empty_output><jupyter_text>Resize Images* https://github.com/python-pillow/Pillow/blob/c9f54c98a5dc18685a9bf8c8822f770492a796d6/_imagingtk.c<jupyter_code>#This one takes IO so multithreading might be better?
def resize_img(fpath, base):
    img = Image.open(fpath)
    rimg = img.resize((224,224))
    img.close()
    return rimg
DATA_PATH='/home/bfortuner/workplace/data/imagenet_sample/'
fnames = list(glob.iglob(DATA_PATH+'*/*.JPEG'))
N = 5000<jupyter_output><empty_output><jupyter_text>Serial<jupyter_code>%timeit -n 1 [resize_img(f,1) for f in fnames[:N]]<jupyter_output>1 loop, best of 3: 14.1 s per loop<jupyter_text>Multithreading<jupyter_code>%timeit -n 1 multithreading(resize_img, fnames[:N], 2)
%timeit -n 1 multithreading(resize_img, fnames[:N], 4)
%timeit -n 1 multithreading(resize_img, fnames[:N], 8)<jupyter_output>1 loop, best of 3: 3.26 s per loop<jupyter_text>Multiprocessing<jupyter_code>%timeit -n 1 multiprocessing(resize_img, fnames[:N], 2)
%timeit -n 1 multiprocessing(resize_img, fnames[:N], 4)
%timeit -n 1 multiprocessing(resize_img, fnames[:N], 8)<jupyter_output>1 loop, best of 3: 4.21 s per loop