Multitasking, Review and Beyond
The last lecture. First one remaining piece of the picture — how a program does several things at once — then sixteen weeks assembled into one map, and finally where this course leads.
By the end of this lecture you should be able to
- State the difference between concurrency and parallelism, and what the GIL is
- Decide whether a task wants threads, processes, or neither
- Assemble the whole course into a single structure
- Know what the final exam covers
- Name the directions this course opens up
Contents
1. Multitasking
Your programs have been a single line
Every program you have written runs as one line of execution: start at the first statement, proceed downward, finish one thing before starting the next.
Real programs frequently need to do several things at once:
- a browser downloads images, renders the page, and responds to your clicks
- a server handles thousands of users simultaneously
- scientific computing splits one large task across eight CPU cores
This section fills that gap — concepts only; no implementation is required of you.
Concurrency and parallelism
The words get used interchangeably and mean different things:
| Concurrency | Parallelism | |
|---|---|---|
| meaning | alternating between several things | doing several things simultaneously |
| cores needed | one is enough | more than one, necessarily |
| analogy | one cook moving between three hobs | three cooks, one hob each |
| what it solves | not wasting time while waiting | too much computation |
One person switching rapidly between tasks, appearing simultaneous — that is concurrency. Several people genuinely working at once — that is parallelism.
Two kinds of task, two kinds of bottleneck
Deciding whether multitasking helps starts with where your task is stuck:
| Kind | Bottleneck | Examples |
|---|---|---|
| I/O-bound | waiting on something external | reading files, fetching pages, querying a database |
| CPU-bound | the computation itself | matrix work, image processing, cracking passwords |
An I/O-bound task spends most of its time waiting. While it waits the CPU is idle — switching to something else during that gap is free money.
A CPU-bound task is always computing. Switching between tasks gains nothing, because one core is only so fast. You need more cores.
import time
# CPU-bound: always computing, every second spent on arithmetic
def cpu_task() -> int:
return sum(i * i for i in range(200_000))
t = time.time()
for _ in range(5):
cpu_task()
cpu_time = time.time() - t
print(f"5 CPU tasks in sequence: {cpu_time:.3f} s")
print("All five hold the CPU throughout - only more cores will speed this up")
print()
print("For contrast: five I/O tasks each waiting 0.1 s take 0.5 s in sequence,")
print("and for almost all of that half-second the CPU sits idle. That idle time")
print("is exactly what concurrency picks up.")
Python’s GIL
Now for something specific to Python.
The GIL (Global Interpreter Lock) is a lock inside CPython guaranteeing that only one thread executes Python bytecode at any moment.
The consequence is direct: in CPython, threads cannot speed up CPU-bound work. Start eight threads doing matrix arithmetic and they still queue for one core.
Three approaches
| Approach | Module | Suits | Affected by the GIL |
|---|---|---|---|
| threads | threading | I/O-bound | yes (but released while waiting on I/O) |
| processes | multiprocessing | CPU-bound | no (each process has its own interpreter) |
| async | asyncio | large numbers of concurrent I/O | yes (but it uses no threads at all) |
Threads: for waiting
A thread waiting on I/O releases the GIL, letting others run. So threads genuinely help I/O-bound work.
import threading
import time
def download(name: str) -> None:
print(f" starting {name}")
time.sleep(1) # pretend to download
print(f" finished {name}")
threads = [threading.Thread(target=download, args=(f"file{i}",)) for i in range(3)]
start = time.time()
for t in threads:
t.start()
for t in threads:
t.join() # wait for all of them
print(f"three downloads took {time.time() - start:.1f} s (not 3)")
Three one-second downloads take about one second, because they wait simultaneously.
Processes: for computing
Each process has its own interpreter and its own GIL, so they genuinely run in parallel.
from multiprocessing import Pool
def heavy(n: int) -> int:
return sum(i * i for i in range(n))
if __name__ == "__main__": # multiprocessing requires this (see L10)
with Pool(4) as pool:
results = pool.map(heavy, [1_000_000] * 4)
print(results)
The cost is that processes share no memory — data is serialized back and forth, far more expensive than with threads. If the tasks are not heavy enough, starting the processes costs more than it saves.
Async: one thread, enormous numbers of I/O operations
import asyncio
async def fetch(name: str) -> str:
await asyncio.sleep(1) # yield control while waiting
return f"{name} done"
async def main() -> None:
results = await asyncio.gather(fetch("A"), fetch("B"), fetch("C"))
print(results)
asyncio.run(main())
async / await let one thread voluntarily give up control while waiting and do something else. No threads means no GIL contention and no thread-switching cost — which is why nearly all modern network servers work this way.
Choosing
Where is your task stuck?
├── waiting on I/O (network, files, database)
│ ├── a handful of tasks → threading
│ └── hundreds or thousands → asyncio
├── computing slowly (CPU-bound)
│ └── multiprocessing
└── neither → do not multitask
# illustrating a race condition: two "threads" incrementing one variable
counter = 0
def increment_unsafe() -> None:
global counter
temp = counter # read
temp = temp + 1 # compute
counter = temp # write
print("Those three steps are not atomic. In genuine multithreading,")
print("two threads may read the same value, each add one, and one increment is lost.")
print("The fix is a lock, and locks in turn can deadlock.")
2. A map of sixteen weeks
Six parts, one line
Part I Foundations of Computation L1-L4
what computation is → what data is → how to express it → how to control flow
closes on: writing a complete program with input, branching, loops, functions
Part II Built-in Data Structures L5-L6
one variable is not enough → five containers → which to pick
closes on: choosing the right container
Part III Problem Solving with AI L7-L8
making problems smaller (recursion) → judging code (AI collaboration)
closes on: decomposing problems and reviewing code
Part IV Abstraction and Organization L9-L10
packaging data with behavior → relationships and files (inheritance, modules)
closes on: structuring a medium-sized program
Part V Robustness and the Real World L11-L12
too slow / crashes / quietly wrong → external data is dirty
closes on: handling real data
Part VI Pythonic Python L13-L16
iteration and generators → advanced functions → AI at project scale → closing
closes on: writing idiomatic Python and delivering a real project
Threads running through everything
Some ideas belong to no single lecture but recur, deepening each time:
One: the reference model. L5 variables as arrows → L6 mutable arguments get modified → L9 mutable class variables → L10 == versus is → L14 mutable defaults. One idea with five faces.
Two: cost awareness. L5 intuition about list operations → L6 choosing containers → L11 big-O made precise → L13 space and generators → L15 an AI will not judge complexity for you.
Three: divide and conquer. L3 functions → L7 recursion → L9 classes → L10 modules → L15 decomposing requirements. One idea at five scales.
Four: verification. L3 print versus return → L8 tests rather than plausibility → L11 pytest and assertions → L15 AI tests cannot verify AI code.
Five: the outside world is dirty. L3 input is always a string → L11 exception handling → L12 encodings, bad rows, paths.
The frequent traps
These were emphasized repeatedly and are the likeliest source of mistakes in both exams and practice:
| Trap | Where |
|---|---|
never compare floats with == | L2 |
/ always returns a float | L2 |
input() always returns a string | L3 |
inside a function, return rather than print | L3 |
| parenthesize bitwise operations mixed with arithmetic | L4 |
| mutable default arguments | L4, L14 |
in-place methods return None | L5 |
[[0]*n]*m shares one row | L5 |
| never modify a container while iterating it | L5, L6 |
(1) is not a tuple; (1,) is | L6 |
| dictionary keys must be immutable | L6 |
| a recursion missing its base case | L7 |
forgetting self | L9 |
forgetting super().__init__() | L10 |
is only against None | L10 |
+= on strings in a loop is O(n²) | L11 |
a bare except: | L11 |
| assertions cannot validate user input | L11 |
"w" truncates the file | L12 |
omitting encoding | L12 |
| a generator can only be traversed once | L13 |
| late binding in lambdas | L14 |
| an assignment makes a name local throughout | L14 |
3. The final exam
Format
Closed book, 50% of the final grade.
Scope
Everything from L1 to L16, unevenly weighted:
| Content | Approximate weight |
|---|---|
| basic syntax and data types (L2–L4) | 20% |
| containers and choosing among them (L5–L6) | 20% |
| recursion (L7) | 10% |
| classes and modules (L9–L10) | 15% |
| complexity, exceptions, testing (L11) | 15% |
| files and text processing (L12) | 10% |
| iterators, generators, advanced functions (L13–L14) | 10% |
Not examined: L15’s AI collaboration method (assessed through the project), L16’s multitasking (concepts only, no implementation), and elaborate regular expressions (basic notation only).
Question types
- Read code, give the output — the frequent-traps table is the source for these
- Find and fix — given buggy code, identify and correct the problem
- Write a function — given a signature and examples. By hand, edge cases included
- State the complexity — of a given fragment, with justification
- Short concept answers — for instance “the difference between
==andis”, “what is duck typing”
4. Where this leads
What you can do now
Sixteen weeks ago many of you had never written a line of code. Now you can:
- turn a problem stated in plain language into a working program
- pick the right data structure and estimate what it costs
- read someone else’s code, judge its quality, and find its bugs
- handle real, unclean data
- write tests that verify your own judgment
- work with an AI rather than being led by one
That is not “knowing Python”. That is knowing how to solve problems. The language is only the vehicle.
Directions from here
Data science and machine learning
Your team project is NumPy in embryo. The route from here: NumPy → pandas (tabular data) → Matplotlib (visualization) → scikit-learn (machine learning) → PyTorch (deep learning)
If AI itself interests you, this is the most direct path.
Algorithms and data structures
L11 was only an opening. A real algorithms course covers the full range of sorting and searching, trees and graphs, dynamic programming, greedy methods.
This is the core of computer science, and the most heavily examined material in interviews. Start with the easy problems on LeetCode.
Software engineering
One person writing hundreds of lines and a team maintaining hundreds of thousands are different problems. You would learn: version control (Git), code review, continuous integration, design patterns, architecture.
Every friction you met in the team project is what this discipline exists to address.
Systems and the layer below
L1’s von Neumann architecture, L10’s bytecode, this lecture’s GIL — all are doorways downward. Operating systems, compilers, computer architecture.
Learning some C or Rust will change how you see that layer entirely.
Other languages
Your second language comes far faster than your first, because languages share far more than they differ:
| Language | Worth learning when |
|---|---|
| C | you want to understand memory and pointers, and what sits under Python |
| JavaScript | you want to build for the web |
| Rust | you want C’s performance with safety guarantees |
| SQL | you will touch a database — which almost every direction does |
Finally
L1 quoted the Zen of Python. Read it once more and see whether it lands differently:
import this
Back then I said these would read as platitudes, and that they would read differently after L13.
Now you know what Simple is better than complex refers to — you have seen nine lines collapse into three.
You know Readability counts is not a pleasantry — you have read your partner’s code.
And you understand why There should be one obvious way to do it keeps coming up — you chose between map/filter and a comprehension.
Good luck in the exam, and good luck with everything you write afterward.
Summary
Multitasking
- Concurrency alternates; parallelism is simultaneous
- I/O-bound wants threads or async; CPU-bound wants processes
- The GIL means CPython threads cannot rescue CPU-bound work
- Multitasking brings races, deadlocks and irreproducible bugs — first ask whether the single-threaded version can be made faster
The course
- Six parts, five threads: the reference model, cost awareness, divide and conquer, verification, and the dirtiness of the outside world
- Revise along the threads, not lecture by lecture
- Be able to explain every entry in the frequent-traps table
Exercises
- For each task, say whether it wants threading, multiprocessing, asyncio or none, and why:
- fetching 500 web pages at once
- resizing 10,000 images
- reading a 100 MB CSV and computing statistics
- a chat server handling 1,000 simultaneous users
- Explain why “eight threads doing matrix multiplication” gains almost nothing in CPython.
- Pick one of the five threads and write a page tracing it end to end, naming the lecture and giving a code example at each step.
- Take any ten entries from the frequent-traps table and write the smallest program that triggers each.
- Take a lab problem you solved earlier and rewrite it with what you know now — shorter, faster, or clearer.
- Write down which direction you intend to take and what you plan to learn next. There is no right answer, but it is worth writing once, seriously.