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.
| Resource | Per thread or shared |
|---|---|
| stack, the chain of function calls | one per thread |
| heap, meaning lists, dicts, objects | shared by all threads |
| open files and sockets | shared |
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.
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.
| Priority | Better choice |
|---|---|
| cheap sharing of in-memory data | threads |
| isolation, so one failure stays contained | separate 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.