Threads and Processes
Do many things at once.
On a busy morning, Ava's program must check the stock of fifty suppliers. Each check waits one second for a reply from the internet. One after another, that takes fifty seconds, with the computer just sitting and waiting. If the waits could overlap, the whole job would finish in about one second. That is concurrency.

- Fifty checks, one second of waiting each
- One after another: fifty seconds
- Overlap the waiting: one second
Threads or processes?
Python gives you two main tools. Threads share memory, and are light, and ideal when your program mostly waits, for the network, or the disk. Processes are separate programs, each with its own memory and its own core, and are ideal for heavy calculations. Python has a rule, the global interpreter lock, that lets only one thread run Python code at a time, so threads do not speed up pure calculations.
Threads
- share memory
- light and quick to start
- best when waiting (I/O)
- limited by the GIL for maths
Processes
- separate memory
- heavier to start
- best for heavy maths (CPU)
- use every core
One after another
First, the slow way. Each supplier check sleeps for a fifth of a second, to pretend to wait for the network. Five checks in a row take one second. We measure the time, rounded, to see it clearly. This is our baseline.
import time
def check(name):
time.sleep(0.2)
return f"{name} ok"
start = time.perf_counter()
results = [check(n) for n in ["a", "b", "c", "d", "e"]]
print(results)
print("seconds:", round(time.perf_counter() - start, 1))['a ok', 'b ok', 'c ok', 'd ok', 'e ok'] seconds: 1.0
Threads with a pool
Now the same job, with a thread pool. The ThreadPoolExecutor starts several threads, and map runs the function on all the items, collecting the results in order. The five checks now overlap, and the total time falls to about a fifth of a second. Same function, a much faster program.
import time
from concurrent.futures import ThreadPoolExecutor
def check(name):
time.sleep(0.2)
return f"{name} ok"
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(check, ["a", "b", "c", "d", "e"]))
print(results)
print("seconds:", round(time.perf_counter() - start, 1))['a ok', 'b ok', 'c ok', 'd ok', 'e ok'] seconds: 0.2
Race conditions and locks
Sharing memory has a danger. When two threads change the same variable at the same time, updates can be lost. This is a race condition. A lock allows only one thread at a time into a sensitive section. Here, four threads add to a counter, safely, protected by a lock, and the answer is always exactly right.
import threading
count = 0
lock = threading.Lock()
def add_many():
global count
for _ in range(50_000):
with lock:
count += 1
threads = [threading.Thread(target=add_many) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(count)200000
Processes for heavy maths
For heavy calculations, use processes. The ProcessPoolExecutor runs the function in separate Python processes, on different cores, so they truly run at the same time. The main guard is required, so that new processes do not start themselves in a loop. Here we sum big ranges in parallel.
from concurrent.futures import ProcessPoolExecutor
def heavy(n):
return sum(i * i for i in range(n))
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=2) as pool:
out = list(pool.map(heavy, [10_000, 20_000, 30_000]))
print(out)[333283335000, 2666466670000, 8999550005000]
Parallel greetings
Your turn. Use a thread pool with the map method, to run a function that returns a greeting for each name in a list. Print the results. Ready? Here is one solution. A function, a pool, and map, collected into a list.
Use ThreadPoolExecutor.map to call greet(name) for three names and print the results.
Show hint
pool.map(function, items)
Show solution
from concurrent.futures import ThreadPoolExecutor
def greet(name):
return f"Hello, {name}!"
with ThreadPoolExecutor(max_workers=3) as pool:
print(list(pool.map(greet, ["Ava", "Sam", "Lee"])))['Hello, Ava!', 'Hello, Sam!', 'Hello, Lee!']
Summary
Nice work. Threads overlap waiting and share memory, so protect shared data with locks. Processes use many cores, for heavy calculations. Pools make both easy. Next, a third way to wait efficiently, and a favourite in modern Python, asyncio.
Key takeaways- Threads: waiting jobs, share memory
- Processes: heavy maths, many cores
- ThreadPoolExecutor / ProcessPoolExecutor