Course outline · 0% complete

0/28 lessons0%

Course overview →

Threads vs processes

lesson 5-1 · ~11 min · 15/28

From lessons 3-1 and 4-1, two processes never see each other's variables, because each process privately owns its entire address space: stack, heap, everything.

That is process isolation at full scope. The safety is excellent, and it makes sharing data between processes expensive, since anything shared has to travel through a file, a pipe, or a socket.

This lesson introduces threads, which trade that safety away on purpose.

A second line of execution in the same process

Sometimes you want two things happening at once inside one program: downloading a file while the interface stays responsive, or handling many web requests together. A second process could do the work, but processes cannot share variables.

A thread is a second line of execution inside the same process.

ResourcePer thread or shared
stack, the chain of function callsone per thread
heap, meaning lists, dicts, objectsshared by all threads
open files and socketsshared

Each thread gets its own stack, all threads in a process share the same heap, and the OS schedules threads just as it schedules processes, pausing and resuming them whenever it wants.

The shared heap is the superpower and the danger. Two threads can build one result together with no copying at all, and two threads can silently corrupt the same data, which is the subject of the next lesson.

one processthread 1own stackthread 2own stackshared heapcounter = 0[orders...]
Two threads inside one process: private stacks, one shared heap. Both dashed arrows point at the same data.

Two threads overlapping in time

Two workers sleep for 0.2 and 0.4 seconds, and the total elapsed time reveals whether they ran together.

import threading
import time

results = []

def brew(name, seconds):
    time.sleep(seconds)
    results.append(name)

t1 = threading.Thread(target=brew, args=("tea", 0.2))
t2 = threading.Thread(target=brew, args=("coffee", 0.4))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
elapsed = time.time() - start

print(results)
print("ran at the same time:", elapsed < 0.55)

Output

['tea', 'coffee']
ran at the same time: True

Total time is about 0.4 seconds rather than 0.6, which is the proof of overlap. They share the results list because the heap is shared, with no copying or message passing involved.

join() means wait here until that thread finishes. Without the joins the main thread would reach the print immediately and report an empty list, since starting a thread does not wait for it.

Starting and joining a pool of workers

One thread per name, started in one loop and joined in another.

import threading

results = []

def fetch(name):
    results.append(name + " done")

names = ["a", "b", "c"]

threads = [threading.Thread(target=fetch, args=(n,)) for n in names]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(len(results), "tasks finished")
print(sorted(results))

Output

3 tasks finished
['a done', 'b done', 'c done']

Building the threads in a list first, then looping to start, then looping to join is the standard shape. Starting and joining in one loop would run the workers one at a time and defeat the purpose.

Two details are easy to miss. args=(n,) needs the comma, since it is a one-element tuple rather than a parenthesized value, and the output is sorted because threads may finish in any order, so the raw list order is not guaranteed.

When threads beat separate processes

Threads are the right choice when the tasks need to work on the same in-memory data cheaply.

A shared heap is the whole point: no copying, no serializing, instant sharing. The costs are the flip side of the same fact, since one thread's crash or memory corruption takes down the entire process.

PriorityBetter choice
cheap sharing of in-memory datathreads
isolation, so one failure stays containedseparate processes

Browsers use separate processes per tab for exactly that isolation reason. A crashed page kills one process and the rest of the browser survives, which would be impossible if every tab were a thread in one process.