In this lecture we begin our investigation of concurrent programming by starting from the beginning with a brief history of computer hardware as it relates to the OS and a large picture of how our hardware makes it all the way down to threads. We then investigate how to create threads in Rust and briefly introduce some of the oddities of Concurrent Programming.
Lecture Date
📅 February 4, 2026
Standard
Concurrent Programming
Topics Covered
OS HistoryProcessesThreadsKernel vs User ThreadsRust Threading
MIT's CTSS (1961) and later Unix (1969) introduced time-sharing:
Each program gets a small time slice (e.g., 10ms)
Timer interrupts force switches even without I/O
Multiple users share one machine interactively
Illusion of having your own computer
Key innovation:Fairness — every program gets a turn, regardless of I/O.
📚 Historical Note: Unix was written in C (1972), making it portable across hardware. This was revolutionary — previous OSes were written in assembly for specific machines.
Era 5: Modern Operating Systems (1980s-Present)
Modern operating systems add:
Preemptive multitasking — OS can interrupt any program, anytime
Virtual memory — Programs think they have unlimited RAM
Kernel/User separation — Privileged code protected from user programs
MMU — Memory Management Unit for address translation
Interrupt Handler — Responds to external events
🔮 Future Connection: We'll explore CPU scheduling — how the OS decides which program runs next.
2. Memory (RAM) — Random Access Memory
Fast, volatile storage for running programs. Much larger than registers, but slower.
Key characteristics:
Volatile — loses data when power off
Random access — any location accessible in same time
Organized into pages (typically 4KB chunks)
Shared between all running programs (with protection)
🔮 Future Connection: We'll explore virtual memory and paging — how each process thinks it has its own memory.
3. I/O and Networking
Communication with the outside world:
Device controllers — Manage specific hardware (keyboard, display)
DMA (Direct Memory Access) — Lets devices access RAM without CPU
Interrupts — Devices notify CPU when they need attention
Network interface — Communication with other computers
🔮 Future Connection: We'll explore device drivers and I/O scheduling.
4. Long-term Storage (Disk)
Persistent storage that survives power loss:
HDD — Spinning platters with mechanical arm
SSD — Flash memory, no moving parts
Much slower than RAM, but permanent
Organized into blocks and files
🔮 Future Connection: We'll explore file systems and disk scheduling algorithms.
The Virtualization Solution
Here's the fundamental challenge of operating systems:
How can ONE set of hardware run MANY programs "simultaneously"?
The answer is virtualization. Each running program (process) gets a virtual view of the hardware. The process "thinks" it has:
Its own CPU (with its own registers)
Its own memory (starting at address 0)
Its own handles onto I/O resources
Its own view of files and storage objects through the OS interface
In reality, the OS multiplexes the real hardware among all processes, switching so fast that each process appears to have its own machine.
Explore the Layers
Use this interactive visualization to explore how virtualization works at different levels:
Layer 0: Physical Hardware
The Foundation
CPU
RAM
I/O
Disk
Privilege Levels
User programs (Ring 3) must use system calls to access kernel services (Ring 0)
Hardware Evolution
1940sVacuum Tubes
1950sTransistors
1960sICs
1970sMicroprocessors
2000s+Multi-core
Hardware got faster, but the fundamental components remained the same
This is the actual hardware — there's only one of each component. The OS kernel has privileged access; user programs must ask permission via system calls.
Processes and the Process Control Block
A process is a running program. It's more than just code — it's the complete execution state:
What Makes a Process?
Component
Description
Code (Text)
The compiled program instructions
Data
Global variables, constants
Heap
Dynamically allocated memory
Stack
Function calls, local variables
Open files
File handles, network connections
Execution state
Register values, program counter
The Process Control Block (PCB)
The OS maintains a Process Control Block for each process — a data structure containing everything needed to pause and resume execution:
PCB Field
Purpose
Process ID (PID)
Unique identifier
Process State
Running, Ready, Blocked
Program Counter
Next instruction to execute
CPU Registers
Saved register values
Memory Info
Page tables, segment info
I/O Status
Open files, pending I/O
Scheduling Info
Priority, time used
Here's how a process's virtual memory is organized:
Context Switching
When the OS switches from Process A to Process B:
Timer interrupt fires (or I/O completes)
Save Process A's registers to its PCB
Load Process B's registers from its PCB
Resume execution at Process B's program counter
This is called a context switch. It takes thousands of CPU cycles — not free!
⚠️ Key Insight: Context switches are expensive. This is why threads exist.
Threads — Lightweight Processes
A thread is an execution unit within a process. Multiple threads share the same process resources but have their own execution state.
Thread vs Process
Aspect
Process
Thread
Creation cost
High (copy memory, etc.)
Low (just new stack)
Context switch
Expensive (~10,000 cycles)
Cheap (~1,000 cycles)
Memory
Isolated
Shared with other threads
Communication
IPC required
Direct memory access
Failure isolation
One crash = one process
One crash = all threads
What Threads Share vs Own
Shared (Process-level):
Code segment (text)
Data segment (globals)
Heap
Open file descriptors
Process ID
Per-thread:
Thread ID (TID)
Register set (including program counter)
Stack
Thread-local storage
Here's how multiple threads share process resources while maintaining their own stacks:
Process Memory (Shared)
Code
Data
Heap
Thread 1
Thread 2
Each thread has its own stack, but they share code, data, and heap
Why Threads?
Cheaper concurrency — Creating a thread is ~100x faster than forking a process
Shared memory — Threads can communicate by sharing data structures
Responsiveness — One thread can handle UI while another does computation
Parallelism — On multi-core CPUs, threads can run truly simultaneously
Kernel Threads vs User-Space Threads
There are two fundamentally different approaches to threads:
Kernel Threads (Native/OS Threads)
Managed by the operating system kernel.
OS scheduler decides when each thread runs
Context switches go through the kernel
Can take advantage of multiple CPU cores
What std::thread uses in Rust
use std::thread;
fnmain() {
lethandle = thread::spawn(|| {
println!("Hello from a kernel thread!");
});
handle.join().unwrap();
}
Pros:
True parallelism on multiple cores
One blocked thread doesn't block others
OS handles scheduling
Cons:
Context switches are relatively expensive
Limited by OS thread limits
Thread creation has kernel overhead
User-Space Threads (Green Threads)
Managed by the language runtime, not the OS.
Runtime scheduler decides when each thread runs
Context switches happen in user space (no kernel call)
Many user-space tasks can be multiplexed onto fewer kernel threads
Roughly the style of abstraction Tokio exposes for async tasks in Rust
use tokio;
#[tokio::main]asyncfnmain() {
lethandle = tokio::spawn(async {
println!("Hello from a Tokio task!");
});
handle.await.unwrap();
}
Pros:
Extremely lightweight (can have millions)
Very fast context switches
Cooperative scheduling (explicit yield points)
Cons:
Can't automatically use multiple cores (need work-stealing)
If one blocks on I/O, may block the kernel thread
Requires runtime support
The Connection to async/await
async/await in Rust is best understood as syntax for building and polling futures. In practice, many runtimes schedule those futures cooperatively, which makes the model feel thread-like even though it is not the same thing as an OS thread:
asyncfnfetch_data() ->String {
// "await" is a yield pointletresponse = make_request().await;
process(response).await
}
Think of await as saying: "This future is not ready yet; the runtime can poll something else for now."
This is a control flow mechanism like if and while:
if — branch based on condition
while — repeat until condition
await — pause until result ready
💡 Mindset Shift: Sequential thinking says "this, then that, then this." Async thinking says "do this when it's ready, not in a fixed order."
Rust Threading Examples
Let's see both threading models in action.
Kernel Threading with std::thread
Basic thread creation:
use std::thread;
use std::time::Duration;
fnmain() {
lethandle = thread::spawn(|| {
foriin1..5 {
println!("Hi from spawned thread: {}", i);
thread::sleep(Duration::from_millis(100));
}
});
foriin1..3 {
println!("Hi from main thread: {}", i);
thread::sleep(Duration::from_millis(100));
}
handle.join().unwrap();
}
Output (non-deterministic!):
Hi from main thread: 1
Hi from spawned thread: 1
Hi from main thread: 2
Hi from spawned thread: 2
Hi from spawned thread: 3
Hi from spawned thread: 4
The exact interleaving depends on the OS scheduler — you'll get different results each run!
Passing Data with move
Threads need to own their data (remember ownership?):
use std::thread;
fnmain() {
letnumbers = vec![1, 2, 3, 4, 5];
lethandle = thread::spawn(move || {
// `move` transfers ownership to the threadletsum: i32 = numbers.iter().sum();
println!("Sum: {}", sum);
});
// numbers is no longer available here!// println!("{:?}", numbers); // ERROR!
handle.join().unwrap();
}
Sharing Data with Arc and Mutex
For shared mutable state, use Arc (atomic reference counting) and Mutex: