Threads and Multithreading
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Threads as lightweight execution paths — covered in Lecture 3 (Processes in Operating Systems)
- Multiprocessor systems and multi-core chips — covered in Lecture 1 (Introduction to Operating Systems)
- User mode and kernel mode — covered in Lecture 1 (Introduction to Operating Systems)
- fork and process creation in Unix — covered in Lecture 3 (Processes in Operating Systems)
- Context switching and process scheduling — covered in Lecture 3 (Processes in Operating Systems)
# Threads and Multithreading
4.1 Why Threads: Motivation and Definition
The previous session covered the difference between a process and a thread. This session dives into threads alone. The roadmap: the multithreading models available, the thread APIs (pthreads, Windows threads, Java threads), implicit threading, the issues that come with multithreaded programs, and finally the operating system support for threads in Linux and Windows. The next session returns to processes alone, with process scheduling and process synchronization.
Why create threads at all? Real applications have many small jobs that can run at the same time, and each job can be handed to its own thread.
Hook. You click "save" in a word processor, and the save happens instantly while the spellchecker still catches your typos — how can one program appear to do two things at once? The answer is that it does not do two things with one execution path: it runs several small paths of execution, called threads, at the same time. Real applications are packed with small jobs that could run simultaneously, and every job like this can be handed to its own thread.
The idea is not new. In the older process-based design, an application that wanted many concurrent conversations had to create a whole new process for each one — copying the code, the data, and all the memory. That is why, before threads became popular, the "one process per request" style of concurrency was known to be slow and memory-hungry. Threads give the same concurrency without that copying bill. The two examples below are the professor's way of making this concrete.
4.1.1 The Word Processor Example
Take a word processing application. While I type some information, I also paste an image, and at the same time the spellcheck runs. Everything happens in parallel, so each of these jobs can be treated as a thread of its own. The threads live inside the same application, and the user cannot see them — from the outside it looks like one program quietly doing everything at once. Inside, the threads do the jobs one by one.
The mapping is explicit: the typing keystrokes get thread T1, the image paste gets thread T2, the spellcheck gets thread T3. All three threads share the same document in memory — the same code (the editor's program text) and the same data (the document buffer). If T3 finds a misspelled word, it can mark it directly in the document that T1 is editing, because both see the same buffer. The user never sees three programs; they see one editor window. The threads are invisible workers inside one application.
4.1.2 The Browser Example
Real-world: a browser on a shopping site. You type a query and the first page loads. You select an item; images and multimedia start playing on the page. Meanwhile you click the submit button to add the item to the cart. These are all separate tasks, and the application does them in parallel with the help of threads. It becomes very easy when they share the code and the data. Only in very few cases does a thread need data of its own — that case comes up later in this session.
Notice what makes the browser example feel so natural: page loading, media playback, and cart operations all work on the same page object, the same session, the same cookies. If they were separate processes, they would need explicitly arranged shared memory or message passing just to agree on what is in the cart. As threads of one process, they read and write the same data by default, with no extra machinery. The professor's aside — "only in very few cases does a thread need data of its own" — is a preview of thread-local storage, which we meet in section 4.11.
A thread — a lightweight process — is a program in action, or a task in execution. More precisely: a thread is a single sequence of instructions that the operating system can schedule independently, carrying its own program counter (the pointer to the next instruction), its own register set, and its own stack, while sharing the code, data, and open files of its process.
The word lightweight matters. A thread carries only what an execution path must own — registers and a stack. Everything else (code, data, files) is borrowed from the process. A process owns everything: a complete memory layout, a file table, stacks, and registers. So a thread is, in the standard textbook phrasing, a basic unit of CPU utilization, and a traditional single-threaded process is just a process that happens to contain exactly one thread.
4.1.3 What a Thread Is
A thread — a lightweight process — is a program in action, or a task in execution. If an application has multiple small tasks to perform, it can create a thread for each, and each thread runs in parallel. From the user's point of view it is as if the same application (the same process) is doing all the work; what really happens is that multiple threads are created and each one does its part.
The definition in plain words: a thread is one running task inside a program. The professor's phrase "a program in action" is deliberate — a static file on disk is not a thread; a thread exists only while the program is being executed. When the application decides it has three small tasks, it creates three threads; the scheduler then decides which thread actually gets a processor at any moment. The user's view ("one application doing everything") and the system's view ("several threads, each doing its part") are both correct — they describe the same situation from the outside and from the inside.
4.1.4 Threads vs Processes: Heavyweight vs Lightweight
Process creation is heavy. When you create a process, you must give it its own address space — the code has to be separate, the data has to be separate, everything is allocated separately for the parent P1 and for the child P2. Threads do not do this. Threads share the address space, and they share the data and the code. That sharing is what makes them light, and it increases efficiency.
To see the difference in concrete terms, think of what a process table entry must describe: memory map, open file table, signal handlers, security context, plus the execution context. Creating process P2 means building a whole new instance of all of that. Creating a thread inside P1 means adding one new execution context — registers and a stack — that points back at the already-existing code and data. That is why thread creation is measured in roughly tens of microseconds while process creation costs an order of magnitude or more. Measured on one Unix-like system, creating a thread takes about ten times less time than creating a process, and switching between two threads of one process takes about five times less time than switching between processes.
| Dimension | Process | Thread |
|---|---|---|
| Address space | Own, separate | Shared with the process |
| Code | Separate copy | Shared |
| Data | Separate copy | Shared |
| Registers | Own | Own (per thread) |
| Stack | Own | Own (per thread) |
| Creation cost | Heavy — allocate everything anew | Light — just a new execution context |
| Communication with siblings | Needs shared memory or messages | Direct, through shared data |
The takeaway from the table: threads are light precisely because they own only what execution needs (registers, stack) and borrow everything else. This is the professor's core contrast of the session, and it returns in section 4.5 when we look at the layout of a threaded process in detail.
4.1.5 Multithreaded Servers
Real-world: most web servers are multithreaded. When a client makes a request, the server does not sit idle waiting for only that one client. It listens and services requests from many clients at once. When the first request arrives, it creates a new thread and hands the request to that thread; when the next request arrives, it goes to the next thread, and so on. This is how the server processes many clients with the help of multithreading.
Worked trace: three clients, one multithreaded server. Suppose the server process is listening on port 80, waiting for requests.
- Client A requests a page. The server creates thread T1 and hands the request to T1; the main listener keeps listening.
- Client B requests an image. The server creates thread T2 and hands the request to T2.
- Client A sends a second request. The server creates thread T3 and hands the request to T3.
- T1 finishes its page, exits; the listener is still active; T2 and T3 continue.
The key step is step 1's second half: the listener resumes listening immediately. The server never stops to finish one client's work before taking the next. If it had been a single-threaded server, client B would have had to wait for client A's entire request to finish — which on a busy site could be a very long wait. The final answer of the trace: one listener thread plus one worker thread per client, all sharing the same server code and the same configuration data.
That example brings out the main benefits of multithreaded programming, which we now take one by one: responsiveness, resource sharing, economy, context switching, and scalability.
Common pitfalls for this concept.
- Thinking threads are visible programs. Threads are not separate applications; they are invisible execution paths inside one process. A user can see three windows of three processes, but never "three windows" of three threads in one process.
- Believing a thread owns the code and data it uses. It borrows them. Only the registers and the stack are the thread's own — everything else is shared with sibling threads. (More precisely, this is the layout we formalize in section 4.5.)
- Assuming process creation is as cheap as thread creation. It is not: the whole address space must be duplicated. This is the entire reason the "process per request" server design was abandoned.
- Concluding that more threads always mean more work gets done. On a single core, threads take turns; parallelism needs multiple cores (section 4.3).
Recap + bridge. A thread is a lightweight, independently schedulable execution path that shares its process's code and data; real applications — word processors, browsers, web servers — use threads to keep many small jobs moving at once. The web server trace just introduced the five payoff areas, so the natural next question is: what exactly does multithreading buy us? The next section answers it benefit by benefit.
In the broader field, multithreading is the default structure of nearly every interactive and server-side system you will meet: web servers and database servers (a thread per request or per connection), GUI frameworks (a UI thread plus worker threads), media players (a decode thread, a render thread, a network thread), and even operating-system kernels themselves, which run kernel threads for device and interrupt management. When you later meet process scheduling and synchronization in the next sessions, keep this section's picture in mind: scheduling decides which thread runs next, and synchronization exists precisely because threads share data so freely.
4.2 Benefits of Multithreaded Programming
The web server trace in section 4.1 delivered the payoff list. The professor takes the five benefits one by one: responsiveness, resource sharing, economy, context switching, and scalability. Keep the web server in mind while reading them — each benefit is exactly the property that server relies on.
Intuition. Think of a restaurant kitchen. The head chef (the main thread) takes orders; the prep cook, the grill cook, and the pastry cook are worker threads. If the pastry cook's oven is broken and she is stuck waiting, the grill cook keeps cooking — the kitchen does not stop. Threads give an application the same resilience: when one thread waits, the others keep working. The analogy's limit: in a kitchen, cooks share a single stove; in a computer, threads only run truly at the same time when there are multiple processors (section 4.3).
4.2.1 Responsiveness
Responsiveness means nothing gets blocked. Suppose a program has four threads. Even if one thread is blocked, the others continue, because each thread is in execution and each is independent — one thread does not affect the other.
A thread becomes blocked when it waits for something — an I/O operation, a network reply, a lock. In a single-threaded program, a blocked program is a frozen program: the user clicks and nothing happens. In a four-thread program, the thread that is waiting is the only one that stops; the other three keep running. This is what a browser uses when an image is loading slowly: the image-loading thread blocks, but the thread that reads your clicks continues, so the page stays responsive. Responsiveness is the user-visible benefit — the application never appears to freeze.
4.2.2 Resource Sharing
Threads share the resources of their process: messages, information, data, and memory. Sharing is far easier between threads of one process than between separate processes, which need their own address spaces.
This is the default behaviour, and that default is the point. Two processes can share data only through machinery the programmer must explicitly build: shared memory regions or message passing, both of which pass through the kernel. Two threads of one process simply use the same memory — if T1 updates a variable, T2 sees the update when it reads that location. The shopping-cart example from section 4.1 works exactly this way: the media thread and the cart thread read and write the same session data without any IPC machinery. The word "messages" in the professor's list refers to the message-based communication that threads can also use within a process — but with shared memory available by default, it is rarely needed between sibling threads.
4.2.3 Economy
Creating threads is cheap. The overhead of creating a thread is much smaller than the overhead of creating a process, so multithreading brings an economy of creation and management.
The cost difference is not symbolic. Because a thread needs only a new execution context (registers and a stack) while a process needs a duplicated address space, thread creation costs a small fraction of process creation: on one Unix-like system, thread creation is about ten times faster than process creation, and thread context switching about five times faster. There is also an economy of management: terminating a thread is as cheap as creating it, and the operating system has less state to maintain for a thread than for a full process. For workloads that create and destroy execution units constantly — a web server handling thousands of short requests — that difference is the difference between an overloaded machine and a responsive one.
4.2.4 Context Switching
When the operating system switches between processes, it has to save the state of the previous process, load the next process into memory, and only then start executing — that sequence creates overhead. In the case of context switching between threads, the overhead is much smaller, because everything the threads need lives in the same address space.
A context switch is the operating system's act of pausing one execution unit and starting another: save the outgoing unit's registers and program counter, load the incoming unit's, and continue. Switching between two processes also requires switching address spaces — the memory maps and page tables that describe where each process's code and data live. Switching between two threads of the same process skips that part entirely: both threads use the same address space, so the switch is just register-and-stack bookkeeping. The professor's sequence — save state, load next, start executing — still happens, but the expensive middle step (loading a new address space) disappears.
4.2.5 Scalability
In multiprocessor architectures, multithreaded programs scale well: with more than one processor available, each processor can take a thread and run it, so the work spreads across the hardware.
Scalability means the application gets faster as you add hardware. A single-threaded process can run on only one processor at a time, no matter how many the machine has. A multithreaded process with enough threads can put a different thread on each processor, so adding processors adds throughput — up to the limit imposed by Amdahl's law (section 4.6). This is the benefit that connects threads to the hardware trend of the last two decades: as chips moved from one core to many cores, only multithreaded software could use the new cores at all.
Common pitfalls for this concept.
- Assuming "runs in parallel" means "runs at the same instant." On a single core, threads take turns (concurrency); true simultaneity needs multiple cores. Benefit 4.2.5 explicitly assumes multiprocessor hardware; the other four benefits hold even on one core.
- Thinking sharing is always safe. Resource sharing is the reason threads are fast and also the reason they can corrupt each other's data — two threads updating the same variable can interleave badly. The cost of sharing is the synchronization problem of the next sessions.
- Believing context switches are free. They are merely cheap relative to process switches; they still save and restore registers and stacks, and switching too often wastes time.
- Expecting scalability without Amdahl's law. Adding cores helps only the parallel portion; the serial portion limits the gain (section 4.6).
Recap + bridge. The five benefits — responsiveness, resource sharing, economy, context switching, and scalability — all follow from one fact: threads share an address space, so they are cheap to create, cheap to switch between, and easy to make communicate. Scalability is the only benefit that needs more than one processor, and that is exactly where the lecture goes next: what hardware do threads actually run on?
In the broader field, these five benefits are the standard evaluation checklist for threading: when engineers debate whether to make a server, a game engine, or a database multithreaded, they weigh responsiveness against synchronization risk, economy against correctness. The database world, for example, runs each connection on its own thread and each query planner as a parallel worker — pure resource sharing plus scalability — and the games industry reports that coarse-grained threading (one module per core) reliably raises performance while fine-grained threading is much harder to get right.
4.3 Multiprocessor and Multi-Core Systems
The previous section ended with scalability — the one benefit that needs multiple processors. Now the lecture looks at the hardware itself: what a multiprocessor system is, why using many cores well is hard, and the two execution styles the hardware enables.
Hook. Your phone has eight cores, and your laptop has ten — yet some apps still feel slow. Why? Because owning many cores and using them are two different problems, and the second one is where software usually fails.
4.3.1 What a Multiprocessor System Is
A multiprocessor system is a system with multiple processors. Today this usually means a single chip that carries multiple cores, and each core is capable of executing work. Each core can run its own process, and the system as a whole gains raw computing power.
Historically, a multiprocessor meant several separate CPU chips wired together. Today the same idea lives inside one chip: a multi-core processor is a single die containing several complete processing units (cores), each with its own execution pipeline and its own cache, sharing the chip. To the operating system each core looks like an independent processor: it can run its own process, and the machine's total computing power is roughly the sum of the cores. A four-core chip can genuinely execute four instructions at the same instant — one per core.
4.3.2 Challenges of Multi-Core Systems
Using many cores well raises challenges that must be handled:
- Divide the activities. An application such as a word processor has many jobs; they must be split up and handed to each core.
- Keep cores balanced. If there are two cores C1 and C2, one must not be overloaded while the other idles. Both should take more or less the same execution time for a given task.
- Split the data properly.
- Avoid data dependency. If C1 has to wait for C2 to update something, the task stops there and has to be suspended. That should not happen.
- Keep testing and debugging easy. When multiple cores run different tasks, tracing a fault should not become a nightmare.
The five challenges, in depth. These five items are the standard multicore-programming checklist, and each one maps to a failure mode:
- Divide the activities — the application must be examined for jobs that can run independently. A word processor splits into keystroke handling, formatting, spellcheck. If you never split, you never use more than one core.
- Balance — the split pieces must do roughly equal work. If C1 runs for 1 second and C2 for 5 seconds, the second core is idle 80% of the time and the "multicore" app is barely faster than single-core. Balance is also about value: a tiny task may not justify occupying an entire core.
- Split the data — each task needs its own slice of the data. If all cores grind on the same dataset, the benefit vanishes.
- Avoid data dependency — if C1 cannot finish without C2's latest update, C1 must wait and the pipeline stalls; the professor's rule is that a task blocked on another's update must be suspended until the data arrives. The coordination needed to satisfy dependencies is synchronization (the subject of later sessions).
- Testing and debugging — with many cores there are many possible execution orders (interleavings); a bug may appear only in one of them, making faults intermittent and hard to trace.
From a multi-core or multiprocessor system you can get either parallelism or concurrency.
4.3.3 Parallelism vs Concurrency
Parallelism means the system performs multiple tasks at the same instant — simultaneously. That requires more than one core, because everything happens at the same time. Concurrency also handles more than one task, but to the user it only seems that many things happen in parallel; inside, the tasks take turns. Concurrency can be provided by a single processor or a single core: the scheduler runs multiple tasks one after another, in a time-sliced manner or however it is arranged, so even with one core you can operate several tasks. Parallelism is the stronger claim — the tasks genuinely overlap in time — and concurrency is the weaker one: interleaving on one core. With multiple cores, both are available; with a single core, only concurrency is.
Analogy: the one-barber shop and the four-barber salon. Concurrency is a shop with one barber and four waiting customers: only one customer gets a haircut at a time, but all four customers make progress — the shop "handles" four people. Parallelism is a salon with four barbers: four customers actually get haircuts at the same instant. The analogy breaks where the professor's definitions meet: a machine with four cores can also run concurrently (the scheduler interleaves tasks across cores), so "concurrency" is about handling multiple tasks, while "parallelism" is about the tasks truly overlapping in time.
The distinction is the lecture's key vocabulary pair of this session, and the professor's phrasing gives the crisp rule: concurrency is about dealing with many things at once; parallelism is about doing many things at once. A single core can be concurrent but never parallel; multiple cores can deliver both.
| Dimension | Concurrency | Parallelism |
|---|---|---|
| Core requirement | Works on one core | Needs two or more cores |
| What happens inside | Tasks take turns (time slicing) | Tasks run at the same instant |
| User's view | Looks simultaneous | Is simultaneous |
| Strength of claim | Weaker | Stronger |
| Available on one core | Yes | No |
Common pitfalls for this concept.
- Using "concurrency" and "parallelism" as synonyms. They are different claims: concurrency is interleaved progress on one core; parallelism is genuine overlap on several. Exam answers that blur this pair lose the key distinction.
- Thinking a multi-core machine always gives parallelism. It offers the capability; the software must split its work into enough independent tasks to use it. A single-threaded program on a 64-core machine is still sequential.
- Believing time slicing means each task finishes faster. On one core the total work takes the same CPU time; slicing only interleaves it.
- Underestimating balance. "More cores" does not mean "proportional speedup" — the five challenges above, especially balance and dependency, are why real speedups fall short.
Recap + bridge. A multiprocessor system — today, a chip with many cores — can execute truly in parallel, but only if the software divides activities, balances them, splits the data, and avoids dependencies; parallelism and concurrency are the two execution styles it can provide. The lecture now asks a sharper question: when a program is parallel, is the parallelism in the data or in the tasks? That split is the next topic.
In the broader field, the parallelism-versus-concurrency vocabulary is the foundation of all parallel computing: chip vendors (Intel, AMD, ARM, NVIDIA) scale core counts to tens and hundreds, and every performance claim about "parallel" software is really a claim about how well the five challenges were solved. Database servers, video encoders, and game engines all report near-linear scaling on carefully balanced workloads — and mediocre scaling when balance or dependency dominates.
4.4 Data Parallelism and Task Parallelism
Section 4.3 established that multiple cores can run tasks in parallel — but where does the parallelism come from? The professor splits the answer in two: parallelism in the data, and parallelism in the tasks.
Hook. A video is 10,000 frames; a film studio wants to render it on 10 machines. The easy way: give each machine 1,000 frames — same job, different data. The hard way: one machine does shading, another does lighting, a third does physics — different jobs, coordinated. Both are "parallel"; only the second is task parallelism.
4.4.1 Data Parallelism
In data parallelism, a subset of the same information is spread across different cores, and the same operation is performed on all of them. For example, take a loop that performs a sum:
This is an operation repeated on the same set of data, so the iteration can be distributed across different cores: each core runs the same operation on its subset of the data. That is data parallelism.
The defining pattern: one operation, many pieces of data. The loop body sum ← sum + i is identical for every i; only the value of i changes. The loop's 9 iterations can be carved up — say core C1 takes i = 1, 2, 3; core C2 takes i = 4, 5, 6; core C3 takes i = 7, 8, 9 — and each core executes the same instruction on its own subset. Image processing, matrix multiplication, and video encoding are data-parallel workloads for exactly this reason: thousands of pixels or matrix entries, one operation.
Worked example: the sum loop, counted precisely. The loop written above runs for i = 1, 2, 3, ..., 9 — that is 9 passes (the condition i < 10 stops the loop before i reaches 10), summing to
The professor introduced the example saying the iteration takes place 10 times; the exact count depends on the loop bounds — with the condition i ≤ 10 (equivalently i < 11), the loop runs 10 passes and sums to 55. So: the spoken description "10 times" matches the i ≤ 10 variant; the written condition i < 10 gives 9 passes and sum = 45. In an exam, count passes from the bounds given, never from memory: passes = (largest i that satisfies the condition) − start + 1.
Sense-check: adding the first 9 natural numbers, 1 + 9 = 10, 2 + 8 = 10, 3 + 7 = 10, 4 + 6 = 10, plus the middle 5 gives 4 × 10 + 5 = 45 — consistent. If three cores split the 9 iterations as (1–3), (4–6), (7–9), each core adds three numbers (6, 15, 24) and the final combination is 6 + 15 + 24 = 45 — the same answer, produced in about a third of the time.
4.4.2 Task Parallelism
In task parallelism, the work itself is different. We distribute threads across the different cores, and each thread performs a unique operation. The word processor and browser examples from the start of this session are task parallelism: typing, pasting an image, and spellchecking are different operations, each given to its own thread.
The defining pattern: many different operations, possibly on different data. Each thread runs its own code — the keystroke handler, the image renderer, the spellchecker. Nothing is duplicated across cores; instead the application's tasks are handed to different threads, and the threads to different cores.
| Dimension | Data Parallelism | Task Parallelism |
|---|---|---|
| What is distributed | The data (subsets of one dataset) | The tasks (different jobs) |
| Operation | Same operation on every subset | Each thread performs a unique operation |
| Typical shape | Parallel loops over arrays, matrices, pixels | Concurrent modules: UI, render, spellcheck |
| Example from the lecture | Sum loop over i = 1..9 | Typing + pasting + spellcheck in a word processor |
| Where it shines | Numbers-heavy, uniform workloads | Mixed-tasking, interactive applications |
Real programs combine both: a browser's spellcheck thread (task parallelism) may itself run a dictionary lookup over many words in parallel (data parallelism). When to pick which: if the work is one uniform operation over lots of data, choose data parallelism — it is the easiest to balance; if the application naturally consists of distinct responsibilities, choose task parallelism — but expect balance to be harder to achieve, since different tasks take different times.
4.4.3 Threads and Cores: Oracle SPARC T4
The number of threads you can run depends on the number of CPUs in the machine. Based on the number of CPUs you can create that many threads, and let each thread execute on one CPU. Real-world: Oracle's SPARC T4 processor has 8 cores with 8 hardware threads per core. With 4 such cores, 4 cores × 8 threads per core = 32 threads can be created. The number is not fixed at 8 or 10 anymore — vendors keep adding cores: chips with 16 cores exist, and NVIDIA and AMD ship up to 64 cores. So the number of threads that can be created multiplies accordingly, across Intel, ARM, AMD, and other architectures.
Worked example: counting threads on the SPARC T4. The rule: the number of threads an application can run in parallel is bounded by the number of hardware execution slots — cores times hardware threads per core.
- Take the Oracle SPARC T4 processor: 8 cores, and each core supports 8 hardware threads.
- A system built with 4 such cores has hardware threads in total.
- So an application can create and run 32 threads at full parallelism on that machine — 32 independent flows of work, one per hardware thread.
Note that the T4's own arithmetic is 8 × 8 = 64 threads on a single chip; the professor's 4-core version is the same multiplication with 4 chips. The general formula:
The point is the trend, not the number: the answer used to be a small fixed value, but vendors keep multiplying — 16-core chips are common, and NVIDIA and AMD ship parts with up to 64 cores, so the thread ceiling keeps rising across Intel, ARM, AMD, and other architectures. Software that stays single-threaded simply leaves those slots empty.
4.4.4 Concurrent vs Parallel Execution
The two execution styles line up with concurrency and parallelism. On a single core, execution is concurrent: start with T1, then T2 continues, then T3, T4. Even if T1 has not finished, in a time-sliced manner it has to come out after its slice — say two seconds — and wait for its next turn; T2 gets the same time, whether it finishes or not. On multiple cores, execution can be parallel: run two threads on one core and another two on another core — T1 and T3 on one, T2 and T4 alternating on the other, working at the same time. When the number of cores grows, the operating system has to provide the support that lets the parallelism grow with it.
This is the lecture's execution diagram, and it is worth drawing in words. One core: the timeline is a single line; at time 0–2s T1 runs, at 2–4s T2 runs, at 4–6s T3 runs, at 6–8s T4 runs, then back to T1 — each thread gets a fixed slice (say two seconds) and leaves the core when its slice ends, whether its work is finished or not. Two cores: the timeline splits into two lines; T1 and T3 share core C1 (interleaved in slices), while T2 and T4 share core C2 — at any instant two threads are genuinely executing, one per core. Adding cores adds lines to the timeline, and the operating system's scheduler is what draws the lines: it must keep enough threads ready to fill every core. That is what the professor means by "the operating system has to provide the support that lets the parallelism grow" — scheduling, load balancing, and the hardware-awareness of section 4.3.
Common pitfalls for this concept.
- Thinking a loop's pass count is optional detail. It is not — it is determined by the bounds:
i < 10fromi = 1gives 9 passes; onlyi ≤ 10gives 10. Always recompute from the written bounds. - Confusing data and task parallelism. Ask: is it the same operation on different data (data), or different operations (task)? Spellcheck is a task; summing pixels is data.
- Believing 32 threads need 32 cores. They need 32 hardware threads — cores times threads-per-core — and on fewer slots they take turns concurrently (section 4.4.4).
- Assuming the OS makes parallelism happen by itself. The operating system schedules and balances, but the application must first split the work — the five challenges of section 4.3 are the application's job.
Recap + bridge. Data parallelism repeats one operation over data subsets; task parallelism runs distinct operations as distinct threads; thread capacity equals cores times hardware threads per core; and single-core execution interleaves while multi-core execution overlaps. The lecture now zooms out from how threads run to what a threaded process looks like inside — what is shared and what is private.
In the broader field, this split is the vocabulary of every parallel framework you will meet later: OpenMP's parallel for is data parallelism, GCD dispatch queues are task parallelism, and GPU programming (NVIDIA CUDA, AMD ROCm) is almost pure data parallelism — thousands of cores, one instruction stream per data element.
4.5 Single-Threaded and Multithreaded Processes
4.5.1 The Layout of a Threaded Process
In a single-threaded process there is only one thread, and that thread uses the process's code, data, files, registers, and stack. In a multithreaded process, the code, the data, and the files are shared by all threads, but the registers and the stack are separate for each thread. The registers are limited by hardware: if the number of CPUs is larger, there are more registers available; otherwise, when multiple threads outnumber the registers per CPU, the registers have to be split, and the same holds for the stack. So the rule to remember: code, data, and files are shared; registers and stack are per-thread.
The one rule of thread layout. In a multithreaded process:
- Shared by all threads: code (the program text), data (global variables, heap, the process's open files and resources).
- Private to each thread: registers (the CPU state the thread was using when it was last running) and the stack (the thread's call history and its local variables).
A single-threaded process is just the degenerate case: one thread owns everything — the code, the data, the files, the registers, and the stack. The process control block describes the process; each thread of it needs a thread control block of its own to hold the thread's registers, priority, and stack pointer, plus its own stack region. The professor's hardware remark captures a real constraint: a CPU has a fixed physical set of registers. When many threads run on few processors, the machine cannot keep every thread's registers loaded simultaneously — each thread's register contents are saved to memory when it leaves a processor and restored when it returns. In that sense the registers are a scarce resource that the threads "split," exactly as they share the limited stack space of the process's address space.
Worked example: three threads inside one process. Take the word processor from section 4.1 with threads T1 (keystrokes), T2 (image paste), and T3 (spellcheck).
- Shared: the editor's program code (T1, T2, T3 all execute the same editor binary), the document buffer and global settings (T3's corrections appear in T1's document), and the open file handles (all three can read the same files).
- Private: T1 keeps its cursor position and call stack on its own stack; T2 keeps the image-decoding routine's stack separately; T3 keeps the dictionary-walk state separately. If T1 calls a function
f(), its local variables go on T1's stack — T2's stack is untouched.
Why this matters: because the stack is per-thread, a thread's function calls cannot collide with another thread's; because code and data are shared, all three threads see one consistent document without any message-passing. The rule in one line: code, data, and files are shared; registers and stack are per-thread.
Common pitfalls for this concept.
- Thinking each thread owns its own code and data. It does not — it borrows the process's code, data, and files. Only registers and the stack are private.
- Believing the stack is shared because threads share the address space. They share the address space, but each thread is given its own stack region inside it. Local variables are private; globals and heap are shared.
- Confusing "registers are per-thread" with "registers are multiplied." Hardware registers are physically fixed per core; "per-thread registers" means each thread has its own saved copy of register contents (in its thread control block) that is swapped in when it runs.
- Extrapolating the sharing rule to everything. Even within one process, thread-local storage (section 4.11) exists precisely for the rare data a thread must keep private.
Recap + bridge. The layout rule — code, data, and files shared; registers and stack per-thread — explains why threads are cheap (they duplicate only registers and stack) and why they communicate easily (everything else is shared). Next, the lecture asks the engineer's question: when we add cores to this process, how much faster does it actually get?
This layout is exactly what operating systems implement in practice: Solaris keeps a process control block with one thread-control-block per lightweight process, Linux's clone() system call takes flags such as CLONE_VM and CLONE_FILES to decide which pieces the new task shares, and Windows XP threads carry a thread ID, register set, user and kernel stacks, and a thread-local storage array — all wrapped in the thread environment block.
4.6 Amdahl's Law
4.6.1 The Speedup Formula
When you add cores to an application, how much faster does it get? Amdahl's law answers that. Let be the serial portion of the application — the fraction that must run one task after another, sequentially — and let be the number of cores. Serial means sequential, one task followed by the next; parallel means everything happens at once. The speedup of the performance gain is:
Here is the serial fraction of the application, and is the number of cores that work on the parallel portion .
Hook. Double the cores and you might expect double the speed. Amdahl's law is the uncomfortable fact that explains why you never get it — and it is the formula companies actually use when they decide how many cores are worth buying.
Why the formula has this shape — the derivation. Speedup is defined as the ratio of two running times:
Call the total single-processor time . Of that time, a fraction is serial: it can only run on one core, so it still takes no matter how many cores exist. The remaining fraction is perfectly parallelizable: spread over cores, it takes . The new total time is so
and the speedup is the old time divided by the new time:
which is exactly the professor's formula. The denominator is the average time per unit of work — the serial part at full cost plus the parallel part discounted by .
Reading the formula. (the serial fraction) is a number between 0 and 1: means a quarter of the execution time can never be parallelized, means the program is fully parallel, means it is fully serial. is the number of cores working on the parallel portion . Two landmarks fall out immediately:
- : — one core gives no speedup, as expected.
- : — a fully serial program gains nothing from any number of cores.
The professor writes for the serial fraction. The reference literature often writes the same law as with = parallel fraction; the two are identical because the professor's equals . Keep the professor's -notation for the exam.
4.6.2 Worked Example: Two Cores with 25% Serial Code
Take cores and a serial portion , which is . Substitute step by step:
So with two cores and 25% serial code, the speedup is 1.6 times. If increases to 4, the speedup grows again — it increases with the number of cores.
Worked example, complete — and extended to 4 cores. Given :
- Step 1 — the parallel fraction: .
- Step 2 — the parallel part spread over cores: with , .
- Step 3 — the new denominator: .
- Step 4 — the speedup: .
The answer: adding one core to a single core gives a speedup of 1.6 times — not 2 times, because the serial quarter drags the average down. Now add two more cores ():
- denominator:
- 2.29 times
Sense-check the pattern: the speedup rose from 1.6 to about 2.29 when the core count doubled from 2 to 4 — it grows, but each doubling gains less than the previous one. That is the diminishing-return signature that section 4.6.3 explains.
Exam note: expect a numerical where you are given (often as a percentage — convert it to a decimal first) and , and you substitute into . The two-core, 25%-serial case, giving 1.6, is the standard template — practise until the four-step substitution is automatic.
4.6.3 The Limit as N Grows
What happens if we keep adding cores? When approaches infinity, the term becomes zero, and the formula collapses to:
The serial portion of the application has a disproportionate — not proportionate — effect on the performance gained by adding cores. If the serial portion were 100%, the whole application would be serial and the formula gives : no gain at all, no matter how many cores. That is why the serial part is the bottleneck.
The limit is the punchline: as , leaving the denominator , so the speedup can never exceed . For the 25%-serial program, that ceiling is — no amount of cores can make that program more than four times faster. Only shrinking raises the ceiling.
Visual intuition. Draw the speedup curve with the number of cores on the horizontal axis and on the vertical axis. The curve starts at (1, 1) and rises steeply at first — for the 25% case, 1.6 at , 2.29 at — then flattens as it approaches the horizontal asymptote . Every doubling of cores adds a smaller increment, and the curve never touches the asymptote. The takeaway in one line: the curve climbs toward and stops; the serial fraction draws the ceiling.
Assumptions & scope. The formula assumes the parallel portion is perfectly parallelizable (no scheduling, communication, or cache-coherence overhead) and that the workload is fixed — Amdahl's law measures speedup of the same problem on more cores. Three things it does not capture:
- Real parallel programs pay overhead — distributing work, synchronizing threads, copying data — which makes actual speedup curve below the Amdahl curve and can even make it turn downward when cores outnumber the work.
- If the problem size grows with the machine (bigger datasets, more users), Gustafson's observation applies: the fixed-workload pessimism is not the whole story for scalable workloads such as servers handling more clients.
- On a machine with few cores, the law is about adding processors; with dozens of cores the diminishing-return conclusion still stands — the serial fraction rules even on a 64-core chip.
4.6.4 Student Questions: Amdahl's Law and Contemporary Multi-Core Systems
Q: Amdahl's law, ma'am — can you please repeat it once?
A: Amdahl's law is mainly used by companies. If I have a multicore system and I need to increase the speedup, this is the law I go for. It was created earlier, and the question that followed is whether this law takes into account the contemporary multicore systems.
Q: What does "contemporary multicore systems" mean?
A: It means the law should be applicable to the multicore systems of every period, including today's. The multicore concept started earlier with two or four cores; now we have many more. Intel started it, maybe 10 to 15 years back, and today NVIDIA and AMD have chips with up to 64 cores. The question is whether the performance gain computed by the law still applies to those systems. And it does: keep increasing the number of cores and the gain approaches 1/S, so the serial fraction still rules, even on a 64-core machine.
Q: A student who had answered earlier realized she had parsed the question differently: I thought you were asking whether the law is useful for multicore systems.
A: No — the law is useful for multicore systems; for that purpose it was created. The question is whether it takes into account today's systems as well. You should refer to the material and confirm this.
The teaching point. "Does the law take into account contemporary multicore systems" asks whether the law stays applicable as hardware evolves — not whether it has a use. The law was designed for multicore speedup; the question is about its validity on today's 64-core hardware. The formula answers yes: as keeps growing, the gain approaches , so the serial fraction still rules, exactly as it did on two-core machines.
Common pitfalls for this concept.
- Using the percentage directly. must become before substitution — , not .
- Confusing with the parallel fraction. is the serial fraction; the parallel fraction is . Some books write the formula with = parallel fraction and = serial; read the notation before substituting.
- Expecting linear speedup. Doubling cores never doubles speed when ; the worked example showed 2 cores give 1.6, 4 cores give about 2.29.
- Mixing up "the law is useful" with "the law still applies." The Q&A exchange above shows this exact confusion: the law's usefulness was never in question — its applicability to today's systems is, and the limit settles it.
- Forgetting the ceiling. The maximum possible speedup is , however many cores you add. A 10%-serial program can never exceed .
Recap + bridge. Amdahl's law, , says adding cores helps only the parallel portion; the serial fraction caps the gain at , so it still rules on today's 64-core machines. With the benefits, the hardware, and the limit in place, the lecture switches from why we thread to how: who manages the thread — the user-level library or the kernel?
In the broader field, Amdahl's law is the standard argument in every performance engineering discussion — chip vendors use it to justify core counts, cloud providers use it to price CPU hours, and database and game-engine teams use it to decide where optimization effort goes: attacking the serial fraction pays off permanently, while adding cores pays off only up to the ceiling.
4.7 User Threads and Kernel Threads
4.7.1 Kernel Threads
A kernel thread is a thread that is executed in the kernel space, and it is supported by the kernel. Real-world: essentially all major operating systems support kernel threads in one way or another — Windows, Solaris, Linux, Unix, and Mac OS.
4.7.2 User Threads
A user thread is a thread that is executed in the user space. It is managed by the user-level thread library, not directly by the kernel.
4.7.3 Thread Libraries
The thread libraries in common use are POSIX pthreads, Windows threads, and Java threads. Pthreads threads and their libraries are examined in detail in section 4.9.
Hook. The same word, "thread," describes two very different things: one the kernel knows about and schedules, and one only your program's library knows about. Which kind you get changes what can run in parallel and what happens when a thread blocks.
The two spaces. Every modern machine runs with two execution spaces: kernel space, where the operating system itself executes privileged code, and user space, where applications run. A kernel thread lives in kernel space: the kernel creates it, keeps its data structures inside the kernel, schedules it, and switches between kernel threads — Windows, Solaris, Linux, Unix, and Mac OS all support them. A user thread lives in user space: it is created and managed by a user-level thread library (a package of routines for creating, destroying, scheduling, and context-switching threads) and the kernel may not even know it exists. The kernel schedules the process that hosts the user threads; the library decides which user thread runs inside the process.
Kernel threads. A kernel thread is a thread that the operating system kernel supports and manages directly: the kernel holds its thread control block, schedules it like a lightweight process, and can run several of them in parallel on different cores. When one kernel thread blocks on I/O, the kernel can schedule another. This is why Windows, Solaris, Linux, Unix, and Mac OS — essentially all major operating systems — provide kernel threads in one form or another: they are the only threads that can exploit multiple processors directly.
User threads. A user thread is a thread executed in user space and managed by the user-level thread library, not directly by the kernel. Creation, scheduling, and switching between user threads are done by library routines — no system call, no mode switch — which makes them extremely cheap. The price: the kernel sees only the process; if one user thread makes a blocking system call, the whole process blocks (the kernel has no other user thread of that process to run), and without kernel support the threads of one process cannot run on several cores at once.
The comparison. The professor's two definitions sit on two axes: where the thread executes (kernel vs user space) and who manages it (kernel vs user-level library). The trade-off table:
| Dimension | User threads | Kernel threads |
|---|---|---|
| Managed by | User-level thread library | Operating system kernel |
| Executes in | User space | Kernel space |
| Creation/switching cost | Very low (no system call) | Higher (kernel involvement) |
| Blocking one thread | Can block the whole process | Kernel can run another thread |
| Parallelism on multicore | Only if mapped onto kernel threads | Direct, several at once |
| Kernel support needed | No | Yes |
| Examples | POSIX Pthreads library, green threads | Windows, Solaris, Linux, Unix, Mac OS threads |
When to pick which: user threads win when you need very cheap threads within one process and do not need true multi-core parallelism; kernel threads win whenever a blocked thread must not stall the process or when the threads must run in parallel across cores. Most modern systems combine them — user threads mapped onto kernel threads — which is exactly what section 4.8 formalizes as the multithreading models.
Common pitfalls for this concept.
- Thinking "user thread" is just a thread made by a user program. The term names where the thread is managed: the user-level library manages it without kernel involvement. Even a kernel thread can be created on a user's behalf.
- Assuming user threads can always run in parallel. Without kernel support underneath, the kernel sees one schedulable unit per process — so user threads interleave, they do not overlap.
- Believing a blocking user thread only blocks itself. In a pure user-level setup, one blocking system call can stop every thread of the process — the kernel has nothing else of that process to run.
- Forgetting the middle path. The common arrangement is combined: a user-level library on top of kernel threads, trading some cost for both cheap creation and real parallelism.
Recap + bridge. Kernel threads are managed by the kernel in kernel space and can run in parallel; user threads are managed by the user-level library in user space and are cheap but limited. The libraries named here — POSIX pthreads, Windows threads, Java threads — are the ones examined in section 4.9. But first, the lecture answers the bridge question: since real systems have both kinds, how do user threads map onto kernel threads?
In practice, this distinction drives how languages and operating systems are built: the Java Virtual Machine maps its threads onto the host's threads (Win32 API on Windows, Pthreads on Linux and Mac OS X), and Solaris historically ran its JVM on green threads — a user-level library — before moving to one-to-one kernel mapping. When you see a framework advertise "lightweight threads" (goroutines, fibers), you are seeing the user-thread idea reappear on top of kernel threads.
4.8 Multithreading Models
Three classic models describe how user threads map onto kernel threads: many-to-one, one-to-one, and many-to-many, plus a variation called the two-level model.
Hook. A program has 10 user threads, and the machine has 4 cores. How many kernel threads sit underneath — 1, 10, or somewhere in between? The answer determines whether the program can use all 4 cores at all. That choice is the multithreading model.
Think of user threads as customers and kernel threads as checkout counters that can actually reach the hardware. The model decides how the customers line up against the counters. With one counter, only one customer is served at a time; with one counter per customer, every customer has a dedicated server; with several counters shared by all customers, service happens in parallel up to the counter count. Keep this picture while the four models are defined.
4.8.1 Many-to-One
The name says it: many user threads are mapped to only one kernel thread. The advantage is simplicity; the disadvantage is that not all threads can run in parallel — even on a multi-core system. Every action needs the kernel: a user thread request must reach the kernel, and from there the hardware. With only one kernel thread, the second user thread, the third, the fourth — all of them have to wait and take turns. So despite owning a multi-core system, true multithreading cannot be achieved. Real-world: Solaris green threads and GNU Portable Threads use this model.
Many-to-one. All user threads of the process map to a single kernel thread. Thread management happens entirely in the user-level library, so creation and switching are cheap and efficient — but the kernel schedules only one unit for the whole process. When a thread makes a blocking system call, the entire process blocks, and at any instant only one user thread is actually executing, even on a many-core machine. The professor's point is sharp: with only one kernel thread, "the second, the third, the fourth" user thread all wait their turn — the hardware's other cores stay idle, and true multithreading cannot be achieved. Solaris green threads and GNU Portable Threads are the canonical users of this model.
4.8.2 One-to-One
Here each user thread maps to one kernel thread. The advantage is that more than one task can run at a time, because every user thread has its own kernel thread underneath. The disadvantage is the overhead of creating as many kernel threads as user threads: if I have 10 threads, I have to create 10 kernel threads. Real-world: Windows, Linux, and Solaris 9 and later use the one-to-one model.
One-to-one. Each user thread gets its own kernel thread. When a user thread blocks, its kernel thread blocks but the kernel can run another kernel thread of the same process — so a blocking call no longer stalls the process. Several threads can also run in parallel on several cores. The price is the professor's arithmetic: 10 user threads means 10 kernel threads must be created and maintained, and kernel thread creation carries real overhead. For this reason, most implementations restrict the total number of threads an application may create. Windows, Linux, and Solaris 9 and later use this model — it is the modern default.
4.8.3 Many-to-Many
Many user threads are mapped to many kernel threads, and the numbers need not match. Five user threads with three or four kernel threads are enough to achieve multithreading on a multi-core system. Real-world: earlier versions of Solaris used this model, and Windows uses it with the thread fiber package.
Many-to-many. Many user threads are multiplexed onto a smaller-or-equal number of kernel threads. The developer can create as many user threads as needed without paying a kernel thread per user thread, while the kernel threads that do exist can run in parallel on a multi-core machine. The professor's concrete case: 5 user threads served by 3 or 4 kernel threads — enough for genuine multithreading on a multi-core system, since up to 3 or 4 threads can execute simultaneously. The developer is free to create many user threads; the kernel threads in the pool are what actually run. Earlier Solaris versions used it, and Windows provides the same idea through its thread fiber package layered on one-to-one threads.
4.8.4 Two-Level Model
The two-level model is a variation of many-to-many. It works like many-to-many, but a user thread is always bound to a particular kernel thread. The number of user threads is still larger than the number of kernel threads, yet each user thread gets the opportunity to be bound to one kernel thread. There is some overhead — one thread or another will be waiting. Real-world: IRIX, HP-UX, Tru64, and Solaris 8 and earlier use the two-level model; Solaris 9 and later switched to one-to-one.
Two-level model. A variation of many-to-many: most user threads are still multiplexed onto a smaller number of kernel threads, but any user thread can be permanently bound to one particular kernel thread. A bound thread (say, a real-time or high-priority thread) is guaranteed a kernel thread of its own and never waits in the shared pool. The professor notes the residual cost — "one thread or another will be waiting" — because the total number of user threads still exceeds the number of kernel threads, so some threads must share and queue. IRIX, HP-UX, Tru64, and Solaris 8 and earlier used this model; Solaris switched to one-to-one in version 9.
The comparison. All four models side by side:
| Model | User threads : kernel threads | Parallelism on multicore | Blocking one user thread | Main cost | Used by |
|---|---|---|---|---|---|
| Many-to-one | many : 1 | No | Blocks whole process | No parallelism | Solaris green threads, GNU Portable Threads |
| One-to-one | 1 : 1 | Yes | Only that thread blocks | One kernel thread per user thread | Windows, Linux, Solaris 9+ |
| Many-to-many | many : many | Yes (up to kernel thread count) | Kernel runs another thread | Scheduling complexity | Older Solaris, Windows fiber package |
| Two-level | many : many + binding | Yes | Kernel runs another thread | Waiting among unbound threads | IRIX, HP-UX, Tru64, Solaris 8 and earlier |
The professor's exam-intel version of this table: Solaris moved from two-level to one-to-one at version 9; Windows pairs one-to-one with a fiber package for many-to-many; green threads give Solaris its many-to-one heritage.
Exam note: remember which operating system uses which model — Solaris moved from two-level to one-to-one at version 9, Windows pairs one-to-one with a fiber package for many-to-many, and green threads give Solaris its many-to-one heritage.
Visual intuition. Draw the models as two rows of circles — user threads on top, kernel threads below — with arrows between them. Many-to-one: a fan of many arrows from above converging into one circle below. One-to-one: identical vertical arrows, 1:1. Many-to-many: a dense web of arrows between unequal rows. Two-level: the same dense web, but one top circle has a single bold arrow locked to one bottom circle. The takeaway: the arrow pattern is the model — count how many bottoms each top can reach, and you know the parallelism.
Common pitfalls for this concept.
- Thinking many-to-one gives parallel execution. It cannot — one kernel thread means one executing thread at a time, even on a 64-core machine. "True multithreading cannot be achieved" is the professor's exact warning.
- Believing many-to-many means equal counts. The numbers need not match; 5 user threads over 3–4 kernel threads is enough for parallelism up to the kernel thread count.
- Blurring two-level with one-to-one. In two-level, most user threads still share kernel threads; only bound threads get a dedicated one.
- Forgetting which OS uses which model. The exam note above is the memory anchor: Solaris 9 is the turning point (two-level → one-to-one), Windows uses fibers for many-to-many, green threads = many-to-one.
Recap + bridge. The four models — many-to-one, one-to-one, many-to-many, two-level — are the mapping choices between user threads and kernel threads, each trading simplicity against parallelism and overhead. The lecture now turns from mapping to programming: with these models in mind, what does a thread library actually look like to the programmer?
In practice this vocabulary explains the threading behavior you observe daily: a JVM on Windows maps each Java thread to a kernel thread (one-to-one), while the same JVM on an older Solaris ran on green threads (many-to-one). Modern Linux NPTL threads are one-to-one kernel threads, and lightweight "goroutine" schedulers in languages like Go are many-to-many arrangements of user-level tasks over a small kernel-thread pool.
4.9 Thread Libraries and Thread APIs
4.9.1 What a Thread Library Does
A thread library is a collection of APIs — application programming interfaces — that help you create and manage threads. There are two ways such a library can work: the library can live entirely in the user space, or a kernel-level library can support the threads that are present in the user level. Either way, the library is what implements thread creation, synchronization, and joining.
Thread library. A thread library is a collection of APIs (application programming interfaces — the function calls a programmer uses to talk to threading facilities) that create and manage threads. It can be implemented two ways:
- Entirely in user space: all library code and data structures live in user space; calling a library function is an ordinary local function call, not a system call. This is the pure user-level approach of section 4.7.
- With kernel support: a kernel-level library is backed by the operating system; invoking a library function typically becomes a system call into the kernel.
Either way, the library owns the three core operations: creation (spawn a new thread), synchronization (coordinate threads sharing data), and joining (wait for a thread to finish). The three libraries in common use are POSIX pthreads, Windows threads, and Java threads — the next subsections examine each.
4.9.2 POSIX Threads (pthreads)
pthreads — POSIX threads — is a standard created by the IEEE. Its first part is the API for thread creation and synchronization. It is a specification, not an implementation: it tells you how to create a thread, how to create a thread library, and how to join — run threads in parallel, then join them. Real-world: most Unix-family operating systems — Solaris, Linux, and Mac OS — use pthreads. pthreads can be implemented at the user level or at the kernel level.
pthreads. pthreads (POSIX threads) is the IEEE standard 1003.1c — an API for thread creation and synchronization. The professor's emphasis is deliberate: it is a specification, not an implementation. The standard says what the API must do — how to create a thread, how to create a thread library, how to join threads so that they run in parallel and then rendezvous — but each operating system decides how to implement it, at the user level or the kernel level. Most Unix-family systems — Solaris, Linux, and Mac OS — implement pthreads; shareware implementations also exist for Windows. Because the API is standard, a pthreads program compiles and runs on any of these systems unchanged, while the underlying implementation differs freely.
4.9.3 A Worked pthreads Program (Sum of Integers)
The classic example is a C program that sums the non-negative integers given on the command line:
- Include the
pthread.hheader file. - Write a runner function; its prototype is given. The data it works on is shared among the threads.
- Declare a thread identifier and an attribute for the thread, both of the types defined in the header — no need to manage that data by hand.
- Get the argument from the command line. If the number of arguments is less than two, print an error and exit. The count of arguments must be two: the name of the program, and the value we supply as the data for the program.
- Get the default attributes for the thread.
- Create the thread. When a thread is created, an ID is given to it and the attributes are supplied; we pass the runner function, and we pass the argument — the data supplied through the command line.
- Once the threads are created, everything runs in parallel. Later the threads are joined, and the join prints the result.
The thread's work: take the parameter supplied on the command line, convert it to an integer, and run a loop that performs the sum of the non-negative integers. When the summation is over, the thread exits and goes to the join, where the sum is displayed.
This program has two threads in total: the main thread, which is the parent, and the created thread, the child. The parent creates the child, then waits for the child to complete, then joins and exits.
Purpose. The classic multithreading exercise: compute the sum of the non-negative integers from 0 up to a bound, where the bound comes from the command line and the summation itself runs in a separate thread while the main thread waits and prints. This one program exercises the entire pthreads API surface the lecture cares about: creation, parallel execution, and joining.
Inputs & outputs. Input: a command line such as ./sum 8 (program name + one integer). Output: the sum printed after the child finishes. Two threads exist in total: the main thread (parent, starts in main()) and the summation thread (child, starts in the runner() function); they share the global variable that holds the sum.
Steps, with the rationale for each.
#include <pthread.h>— every pthreads program must include the header that declares the types and functions.- Write the
runner()function — this is where the child thread begins execution; its prototype is fixed by the API. Its data (the sum) is global, so it is shared by both threads. - Declare a thread identifier
pthread_t tidand an attribute objectpthread_attr_t attr— the header defines both types, so no manual memory management. - Check
argc: if the argument count is less than two, print an error and exit. The expected count is exactly two: the program name (argv[0]) and the value to sum (argv[1]). - Call
pthread_attr_init(&attr)— this sets the default attributes (stack size, scheduling information); we did not customize anything, so defaults are used. - Call
pthread_create(&tid, &attr, runner, argv[1])— the four arguments are: the thread ID (filled in by the call), the attributes, the function where the new thread begins, and the argument passed to that function (the command-line value). - Call
pthread_join(tid, NULL)— the parent now waits for the child to complete; when the child returns, the join finishes and the parent prints the shared sum.
The child thread's work: convert argv[1] to an integer, run the summation loop over the non-negative integers, store the result in the shared sum, and exit — after which the parent's join returns and the sum is displayed.
Trace: ./sum 8 on a two-thread program. Run the program with the bound 8.
main()starts;argc = 2passes the check (program name +"8").pthread_attr_initsets default attributes;pthread_createcreates the child thread, which begins inrunner()with the argument"8".- The child converts
"8"to the integer 8 and runs the loop: sum = 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 = 36. - Meanwhile the parent sits in
pthread_join, waiting — it does not print anything yet, because the sum is not ready. - The child finishes and exits; the join returns; the parent prints 36 and exits.
Sense-check: the formula for is ; for that is — the trace is right. Note the structure: creation → parallel execution → join — the three operations of section 4.9.1, in the professor's exact order.
4.9.4 Handling Many Threads with an Array
For 10 threads or more, use an array of threads. Each element of the array takes care of one thread, and each thread does its own work separately — the data slots W1, W2, and so on, each handled by its own thread. Later the threads are checked and joined.
Worked pattern: ten threads in one array. Declare pthread_t workers[10]. Each element workers[i] is one thread. Create them in a loop — pthread_create(&workers[i], ...) with work slot W_i for thread i — so thread 0 handles W1, thread 1 handles W2, and so on, each doing its own piece of the data. Later, join them in a loop: pthread_join(workers[i], NULL) for i = 0..9. The parent waits for every child this way, one by one; only after all ten joins return is all the work guaranteed complete. The pattern is: one array, one element per thread, create in a loop, join in a loop. The same loop-and-join pattern is what a web server uses with its per-request threads before shutdown.
4.9.5 Windows Threads
The same addition example, in Windows syntax. Include windows.h; create a variable sum with the data type DWORD, and a summation function that takes care of the adding. The syntax differs, but the multithreading structure is what matters:
- Create the thread with
CreateThread, passing the security attributes (none as of now), the stack size (nothing), the thread start function, the parameter to pass, a default flag of zero — meaning the thread is not yet started — and the identifier for the thread. - If the created thread is not null, it does its work; otherwise print an error and exit.
- If the thread was created successfully, a handle is returned; the return value is checked.
- Once the handle is valid, the child thread has been created and performs its work. Then
WaitForSingleObjectdoes the same job aspthread_join: it waits for the child to complete. It can wait for any number of threads until everything completes. - Close the handle, print the sum, and finish.
Worked walkthrough: the Windows version of the sum program. The structure mirrors the pthreads program — only the syntax differs.
#include <windows.h>; declareDWORD Sum(an unsigned 32-bit integer) globally so both threads share it; write theSummation()function that computes the sum in the child.- Call
CreateThread(NULL, 0, Summation, param, 0, &threadID). The six arguments: security attributes (none, soNULL), stack size (default, so 0), the thread start function (Summation), the parameter to pass, a creation flag of 0 (meaning "not suspended" — the thread is eligible to run immediately), and a pointer to the thread identifier. - Check the returned handle: if the thread was created, the handle is valid; if not, print an error and exit.
- With a valid handle, the child runs and computes the sum. The parent calls
WaitForSingleObject(handle, INFINITE)— the Windows equivalent ofpthread_join: it blocks until the child thread has exited. Because it takes a handle, the parent can wait for any number of threads (waiting on each handle, or usingWaitForMultipleObjects). CloseHandle(handle)releases the handle, the parent prints the sum, and the program finishes.
Sense-check: the five-step skeleton — create, check handle, wait, close, print — is exactly the professor's numbered list, and it matches the pthreads flow step for step: CreateThread ↔ pthread_create, WaitForSingleObject ↔ pthread_join. Learn one structure, adapt the syntax.
4.9.6 Java Threads
Java provides multithreading through an interface called Runnable. We implement this interface — in effect, we extend the thread class by implementing the Runnable interface. There is an abstract method, run(), which we implement in the summation case, because the summation performs the sum of the integers. The sum value is supplied and read with setSum and getSum. The key point: run() is never called directly. It is called with the help of the function start() — whenever start() executes, it calls run(), which executes the task. Later we get the sum of the object with getSum.
Java threads via Runnable. Java's threading model is interface-based. A class implements the Runnable interface — in effect the professor's "extending the thread class" — and must define its abstract method run(). In the summation case, run() performs the sum of the integers. The sum itself is held in a small helper object exposed through setSum and getSum, because Java has no global data: threads share data by sharing object references.
The one rule to internalize: run() is never called directly. Creating a Thread object does not create the thread — calling start() does. The start() method (1) allocates and initializes a new thread in the JVM and (2) calls run() on that new thread, making it eligible to execute. If a programmer accidentally calls run() directly, the summation runs on the calling thread — no new thread, no parallelism, a classic bug. The result is later read with getSum(), and the parent can wait for the child with join().
Worked walkthrough: the Java sum program.
- Define class
Summation implements Runnable, with a reference to a sharedSumobject. run()converts the stored parameter to an integer, sums , and stores the result viasetSum.- In
main(), create aThreadobject, passing theRunnableto the constructor. - Call
start()on that object — the JVM creates the child thread and invokesrun()on it. (Callingrun()directly would execute the sum in the main thread — no parallelism.) - The main thread may call
join()to wait for the child, then reads the result viagetSum()and prints it.
Sense-check: with bound 8, run() computes 36, setSum(36) stores it, and getSum() returns 36 — the same answer as the pthreads and Windows versions, produced through the same create → run → join structure with Java's interface-based syntax.
The comparison. Three libraries, one structure:
| Step | pthreads (C) | Windows (C) | Java |
|---|---|---|---|
| Header/import | pthread.h |
windows.h |
java.lang.Thread |
| Work function | runner() |
Summation() |
run() in Runnable |
| Create | pthread_create |
CreateThread |
new Thread(r) + start() |
| Shared data | Global sum |
Global DWORD Sum |
Shared Sum object, setSum/getSum |
| Wait for child | pthread_join |
WaitForSingleObject |
join() |
| Get result | Read global sum |
Read global Sum |
getSum() |
When to pick which: pthreads for Unix-family portability (Solaris, Linux, Mac OS), the Windows API for Windows-native performance, Java for cross-platform, managed-memory applications. All three implement the same conceptual structure: write the task, create the thread, run, join, read the result.
Common pitfalls for this concept.
- Calling
run()directly in Java. It executes the task on the calling thread — no new thread is created.start()is the only correct entry point. - Forgetting
pthread_join/WaitForSingleObject. Without the wait, the parent may print the sum before the child finishes computing it — a race on the shared data. - Skipping the argument-count check. If
argc < 2, there is no bound to sum; the program must print an error and exit before touchingargv[1]. - Thinking pthreads is a software package. It is an IEEE specification; each OS implements it (user-level or kernel-level) in its own way.
- Ignoring shared data safety. In all three libraries the sum is shared; if both threads wrote it concurrently without coordination, the result could be corrupted. In this example the join prevents that — synchronization in general is the subject of later sessions.
Recap + bridge. A thread library is an API for creation, synchronization, and joining; pthreads is an IEEE specification used by most Unix systems, Windows has its own API (CreateThread, WaitForSingleObject), and Java uses the Runnable interface with start() invoking run(). All three programs sum integers with the same create → run → join structure. So far every thread was created by hand — the lecture now asks: what happens when the thread count grows so large that manual management stops working?
In practice, pthreads underlies nearly every Unix service (Apache, PostgreSQL, and Nginx worker models), the Windows thread API is what .NET and native Windows services build on, and Java threads power application servers such as Tomcat and WebSphere. Knowing the one shared structure — create, run, join — transfers across all of them.
4.10 Implicit Threading
4.10.1 Why Implicit Threading
As a process does more things, the number of threads grows. With explicit threads, everything is manual: create the threads, manage them, later join them. When the thread count climbs, that becomes difficult to handle. Implicit threading hands the work to the compiler and runtime instead of the programmer. Real-world: thread pools, OpenMP, and Grand Central Dispatch (GCD) are the examples; Java also supports implicit threading.
Hook. Section 4.9 ended with hand-crafted threads: declare, create, join. Now imagine a server spawning a thousand threads a second, each with its own creation cost, lifetime, and cleanup. No programmer can hand-manage that — so the compiler and runtime take over. That delegation is implicit threading.
The contrast is the professor's: with explicit threads the programmer does everything manually — create the threads, manage them, later join them — which becomes difficult to handle as the thread count climbs. With implicit threading the programmer just describes what should run in parallel, and the compiler and runtime decide how many threads to create, when, and how to schedule them. The lecture's examples are thread pools, OpenMP, and Grand Central Dispatch; Java's own concurrency utilities support the same idea.
4.10.2 Thread Pools
A thread pool keeps a pool of threads ready instead of creating threads on demand. Not every thread is assigned work at the same time: the first thread is taken and assigned a task; when the next task arrives, it is assigned to the next thread, and so on, depending on how many tasks arrive. This is faster than creating a new thread for each task — the pool already exists, so picking a thread and assigning a task is easy. The number of threads in the pool varies with the application. Real-world: Windows 8 supports thread pools with pool functions — a pool function gets a thread from the pool and performs the task on it.
Thread pool. A thread pool creates a number of threads at process startup and keeps them ready, instead of creating a thread per task on demand. As tasks arrive, the first free pool thread is taken and assigned a task; when the next task arrives, it goes to the next thread; finished threads return to the pool and wait for more work. If every pool thread is busy, the arriving task waits until one frees up. This is faster than creating a thread per task — the pool already exists, so picking a thread and assigning a task is nearly instant — and it bounds the number of live threads, preventing a flood of requests from exhausting memory or CPU.
The pool size is not fixed by the system: it varies with the application, tuned from factors such as the number of cores, available memory, and expected concurrency. Windows 8 supports thread pools with pool functions: the programmer hands a function (with its parameter) to the thread pool API, and a thread from the pool executes it — exactly the professor's description. Java's java.util.concurrent package provides the same utility.
Worked trace: a pool of 3 threads serving 5 tasks. Tasks arrive as T1, T2, T3, T4, T5.
- Pool has 3 idle threads: P1, P2, P3.
- T1 arrives → assigned to P1 (P1 busy). T2 arrives → assigned to P2. T3 arrives → assigned to P3. All three threads now work.
- T4 arrives → no idle thread; T4 waits for a free worker.
- P2 finishes T2 and returns to the pool → T4 is assigned to P2.
- P1 finishes T1 → T5 is assigned to P1.
Sense-check: only 3 threads ever exist, no matter how many tasks arrive; task 4 waits only until a worker returns. Compare with creating one thread per task: 5 threads created and destroyed instead of 3 reused ones — the pool saves the creation cost on tasks 4 and 5 and caps resource use.
4.10.3 OpenMP
OpenMP is a set of compiler directives — like preprocessor directives — available in C, C++, and Fortran. It provides parallel programming by identifying parallel regions so that the code inside them runs in parallel. The identification is done with #pragma omp. Example: add the values of two arrays in parallel and store them into a third array; write it as #pragma omp parallel for. As soon as the directive line is reached, a particular thread starts and executes the work — as many threads as there are cores — and the block of code executes on each core in parallel.
OpenMP. OpenMP is a set of compiler directives — lines the compiler interprets, similar to preprocessor directives — available in C, C++, and Fortran. The programmer marks parallel regions with #pragma omp, and the compiler and runtime turn those regions into parallel execution. The canonical data-parallel pattern from section 4.4:
#pragma omp parallel for
for (i = 0; i < n; i++) {
C[i] = A[i] + B[i];
}
As soon as the directive line is reached, the runtime starts a team of threads — as many as there are cores — and the loop iterations are distributed across them: each core executes the same addition on its own slice of the arrays. The programmer never calls pthread_create; the directive is the entire threading interface.
Worked example: adding two arrays on 2 cores. Let A = [1, 2, 3, 4], B = [5, 6, 7, 8], so C[i] = A[i] + B[i] for i = 0..3.
- Core C1 takes i = 0, 1: C[0] = 1 + 5 = 6, C[1] = 2 + 6 = 8.
- Core C2 takes i = 2, 3: C[2] = 3 + 7 = 10, C[3] = 4 + 8 = 12.
Final result: C = [6, 8, 10, 12]. Sense-check: each element is the sum of its two inputs, and the two cores produced disjoint halves in parallel — data parallelism exactly as section 4.4 defined it, driven by one directive instead of manual thread code.
4.10.4 Grand Central Dispatch (GCD)
Grand Central Dispatch is the technology used in Mac OS and iOS. Libraries help identify the parallel sections. First we identify the blocks — in C, C++, or Objective-C a block starts with { and ends with }, and it is marked with the caret symbol ^ in front of it. Each block is placed in a queue; if a thread is available in the pool, the block is removed from the queue and assigned to that thread. There are two types of queues: a serial queue, which works in FIFO (first in, first out) order — the block that comes first is executed first, and only one block is removed and given to an available thread at a time; and a concurrent queue, which is also FIFO but can remove several blocks in parallel and work on them at once. The serial-versus-concurrent distinction is the same as the synchronized-versus-asynchronous distinction in dispatch.
Grand Central Dispatch (GCD). GCD is Apple's implicit-threading technology for Mac OS and iOS, built on a thread pool. The programmer splits the work into blocks: in C, C++, or Objective-C a block is a chunk of code starting with {, ending with }, marked with a caret ^ in front — ^{ ... } — which packages the code and its captured data into one self-contained unit. Each block is placed on a queue; whenever a pool thread is available, the first block is removed from the queue and assigned to that thread. Two queue types matter:
- Serial queue: FIFO (first in, first out) order — the block that arrives first is executed first, and only one block is removed and given to a thread at a time. The next block starts only after the previous one finishes, so a serial queue serializes work.
- Concurrent queue: also FIFO for departure order, but it can remove several blocks at once and run them on several threads in parallel.
The professor's mapping: serial is the synchronized style of dispatch, concurrent is the asynchronous style — a serial queue guarantees ordered, non-overlapping execution; a concurrent queue allows overlapping execution on many threads. GCD automatically sizes its thread pool to the machine's cores, which is what makes it "implicit": the developer supplies blocks, and the system decides the threads.
Worked trace: three blocks F, G, H on the two queue types. Blocks arrive in the order F, G, H.
- Serial queue: F is removed first and runs; G can be removed only after F completes; H only after G. Execution order is strictly F → G → H, one at a time.
- Concurrent queue (2 threads): F and G are removed together and run in parallel; H starts as soon as either thread frees up. Order of completion is not guaranteed, even though departure from the queue is FIFO.
Sense-check: the queue type — not the blocks — decides the concurrency: same three blocks, serial gives order, concurrent gives overlap. This is why GCD serial queues are the standard tool for protecting shared state (the UI, for example) while concurrent queues handle independent work.
4.10.5 Student Question: Thread Pool Dependencies
Q: Suppose the pool has 10 threads, and since we are talking about multi-core systems, all of them run in parallel. But what if any thread has a dependency on another thread?
A: If the dependency is there, it has to wait — that is the same problem we saw earlier. Threads share the code, the data, and the files; only the stack and the registers are separate for each thread. If a thread uses its own registers, there is no problem — each thread has its own registers and stores its values there. But when threads share data in memory and one thread updates it, the next thread has to wait. In that case we cannot achieve parallelism; concurrency may be achieved, but not parallelism. It all depends on where the data is stored — in memory or in registers.
The teaching point. The student's premise — "10 pool threads on a multi-core system, all run in parallel" — is exactly what breaks when threads depend on each other. The professor's correction: the question is where the data lives. If each thread's working values sit in its own registers, there is no interference — the threads genuinely run in parallel. But if the threads share data in memory and one updates it, the dependent thread must wait for that update, and the wait forces the tasks to take turns. The result degrades from parallelism to concurrency. The lesson ties back to section 4.5's layout rule: only the stack and registers are private; everything in memory is shared, and shared data is where dependencies live.
Common pitfalls for this concept.
- Thinking implicit threading means no threads at all. There are still threads — the runtime just creates and manages them for you (pool threads, OpenMP teams, GCD workers).
- Assuming a thread pool guarantees parallelism. With a 10-thread pool on a 4-core machine, at most 4 threads run at once; the rest wait. And with data dependencies, even 10 threads may reduce to sequential turns — the professor's Q&A above.
- Believing a concurrent GCD queue preserves execution order. It guarantees FIFO departure from the queue, not completion order; blocks may finish out of order.
- Calling
run()directly in Java's implicit-threading utilities. The samestart()rule of section 4.9 applies; submitting a task does not start it until the executor picks it up.
Recap + bridge. Implicit threading delegates thread management to the compiler and runtime: thread pools keep workers ready and bound resource use, OpenMP turns #pragma omp parallel for into multi-core execution, and GCD runs blocks on serial or concurrent queues. The student Q&A exposed the seam where even implicit threading must wait — shared data dependencies. That seam is the subject of the next section: the issues that come with multithreaded programming.
In practice, implicit threading is the modern default: web frameworks and database drivers submit tasks to thread pools rather than creating threads; C++'s standard parallelism, Java's executors, and Python's ThreadPoolExecutor are the same idea; and on Apple platforms GCD queues are the idiomatic way to keep the UI responsive while background work runs.
4.11 Issues in Multithreaded Programming
4.11.1 fork and exec in Multithreaded Programs
fork creates a child process. If the fork is successful it creates the child and returns zero; if it fails it returns a non-zero value, and the child gets its own separate ID. In a multithreaded program, fork raises a question: should it duplicate only the thread that called it, or all the threads present in the process? In Unix there are two versions. One duplicates all the threads — if there are 10 threads, all 10 are duplicated. The other duplicates only the thread that invoked the fork call. Then we can run exec — the exec function replaces the current program with a new program: whatever is specified as the parameter inside the exec call is what gets executed, and only that is replaced. If exec is called immediately after fork, there is no need to duplicate all the threads, because exec will throw them away and load the new program anyway; it is enough to duplicate only the thread that invoked it. But that is not right for every application — if a process forks and never calls exec, all the threads may need to be duplicated. Which version to use depends on the application.
Hook. In a single-threaded program, fork is simple: one process becomes two identical processes. In a 10-thread program, fork becomes a question with two answers — and picking the wrong one either wastes the machine or silently loses the process's other threads.
In a single-threaded program, fork creates a child process: on success it returns zero to the child and the child's ID to the parent; on failure it returns a non-zero value. In a multithreaded program the question is what the child contains: only the calling thread, or every thread of the process? Unix answers with two fork versions, and the professor's rule picks between them:
| Situation | Version to use | Why |
|---|---|---|
exec is called immediately after fork |
Duplicate only the calling thread | exec replaces the whole process image with the new program anyway; duplicating the other threads is wasted work — they are thrown away the moment exec runs |
The process forks and never calls exec |
Duplicate all threads | The child keeps running the same program, so it needs all the state the parent had — all threads and their data |
The key fact about exec is that it replaces the current program: whatever is named in the exec call is what gets executed, and nothing else survives. So the two versions exist because the two futures of the child differ. "Which version to use depends on the application" — the professor's summary is the decision rule itself: look at what the child will do next.
4.11.2 Signal Handling
A signal is information sent to the operating system to notify it that an event has occurred. When a process is running and an interruption takes place — a hardware interrupt or a software interrupt — that event is notified with the help of a signal. The signal handler takes care of processing signals, whether the signal is generated by an event or delivered to a process. There are two handlers: a default handler, which every signal has and which runs when the signal is generated, and a user-defined handler, which can always overwrite the default handler. In a multithreaded program, the question is where the signal should be delivered: to T1, T2, T3, or T4? The options: deliver it to the thread to which the signal applies — for a hardware event, the signal goes to the corresponding thread, not to all threads; or deliver it to each and every thread; or assign a specific thread, say T1, to receive all the signals. So signal delivery is one of the threading issues to decide.
A signal is a notification — "information sent to the operating system to notify it that an event has occurred." A running process may be interrupted by a hardware interrupt or a software interrupt, and the event is announced with a signal. A signal handler is the code that processes a delivered signal, and every signal has a default handler (run by the kernel) which a user-defined handler can always overwrite. In a single-threaded process, "deliver the signal to the process" is unambiguous; in a multithreaded process the question is which thread: T1, T2, T3, or T4?
Signal delivery options. The professor's options, and when each fits:
- Deliver to the thread to which the signal applies — the natural choice for synchronous signals. A synchronous signal is caused by the running thread's own action (illegal memory access, division by zero); it must reach the thread that caused it, not some unrelated thread.
- Deliver to each and every thread — fits asynchronous, process-wide events. An asynchronous signal comes from outside the process (pressing Ctrl+C, a timer expiring); terminating the process means telling every thread.
- Assign a specific thread (say T1) to receive all signals — a dedicated signal-handling thread that serializes handling in one place.
The pattern behind the options: synchronous signals (from the thread's own action) follow the thread; asynchronous signals (from outside) follow the process. Most multithreaded Unix systems let each thread specify which signals it accepts and which it blocks; since a signal needs handling only once, it is typically delivered to the first thread found that is not blocking it. The standard Unix delivery call is kill(pid, signal) for a process and pthread_kill(tid, signal) for a specific thread. Windows has no direct signals; it emulates them with asynchronous procedure calls (APCs) delivered to a specific thread.
4.11.3 Thread Cancellation
A thread can be cancelled — terminated — as soon as its work is finished. The thread that is going to be cancelled is called the target thread. There are two approaches: asynchronous cancellation ends the target thread immediately; deferred cancellation checks periodically whether the thread has to be cancelled. In pthreads, cancelling a thread takes just its ID: pthread_cancel(target_thread_id). Whether the cancellation is possible depends on the state and the mode of the thread. If the mode is not present, the state is disabled, and no cancellation of any type is possible — the state has to be enabled to cancel a thread. If the state is enabled and the mode is deferred, deferred cancellation is available; deferred is also the default. A deferred cancellation takes effect at a cancellation point — for example, the function pthread_testcancel — which performs the cleanup operation so that the thread cancellation is handled. In Java, cancellation is handled through signals. If the mode is asynchronous and enabled, the thread is cancelled immediately by calling the particular function.
Thread cancellation means terminating a thread before its work is complete — for example, when several threads search a database and one finds the answer, or when the user presses Stop while a browser loads a page with one thread per image. The thread to be cancelled is the target thread. Two approaches exist, and the professor's state-and-mode framework decides which applies.
Asynchronous vs deferred cancellation. Asynchronous cancellation ends the target thread immediately — one thread terminates the target on the spot. Deferred cancellation makes the target thread check periodically whether it should terminate, so it can exit in an orderly fashion.
In pthreads the API is minimal: pthread_cancel(target_thread_id) — just the ID. But whether the call can even take effect depends on two per-thread settings:
- State (enabled / disabled): if the state is disabled, no cancellation of any type is possible — the state must be enabled first.
- Mode (asynchronous / deferred): if the state is enabled and the mode is deferred, deferred cancellation is available. Deferred is the default. If the mode is asynchronous and the state is enabled, the thread is cancelled immediately when the function is called.
| State | Mode | What happens |
|---|---|---|
| Disabled | any | No cancellation of any type is possible |
| Enabled | Deferred (default) | Cancellation takes effect at a cancellation point — e.g. pthread_testcancel — where the thread performs cleanup and exits |
| Enabled | Asynchronous | The thread is cancelled immediately |
Why deferred is the safe default: asynchronous cancellation can strike in the middle of updating shared data or while the thread holds a resource, and the operating system reclaims system resources but not all user-level resources. Deferred cancellation confines the exit to points the thread controls, where it can clean up. In Java, cancellation is handled through interrupt signals on the target thread; the thread checks its interrupt status cooperatively.
4.11.4 Thread-Local Storage (TLS)
Threads usually share the data, the code, and the files of the process. But certain threads need their own data; that is called thread-local storage (TLS). TLS is useful exactly when we do not have control over thread creation — for instance, when we are using a pool of threads, we do not create a thread for every task, so the pool's threads need a place to keep their own values. TLS lasts for the entire execution of the thread, and it stays visible across different invocations of the same function: if a local variable is set, it ceases to exist when we come out of the function, but if the same function is called multiple times by a particular thread, the TLS value remains visible across those invocations. Think of TLS as static data that is unique for each and every thread.
Thread-local storage (TLS). Threads usually share the process's data, code, and files — but a few kinds of data a thread must keep for itself, and thread-local storage (TLS) is that private space. TLS is precisely the "only in very few cases does a thread need data of its own" preview from section 4.1.
The professor's characterization is exact: TLS is static data that is unique for each and every thread. Like a static (global) variable, a TLS value is created once and lives for the whole execution of the thread — it does not vanish when a function returns. Unlike a static variable, the value is not shared: each thread has its own copy. Ordinary local variables are created and destroyed with each function call; a TLS value set inside one invocation of a function is still there on the next invocation of that same function by the same thread — and is invisible to other threads.
TLS matters most exactly where we do not control thread creation: with a thread pool (section 4.10), a thread is not created per task, so we cannot pass per-thread state through a constructor or argument. The pool's threads need a place to keep their own values across tasks — transaction-processing systems, for instance, give each thread a unique transaction identifier stored in TLS. pthreads and Win32 both provide thread-specific-data APIs, and Windows XP stores a TLS array in each thread's environment block.
Common pitfalls for this concept.
- Confusing TLS with ordinary static data. Static data is one copy shared by all threads; TLS is a separate copy per thread. Shared static data is where race conditions live; TLS is immune because nobody shares it.
- Thinking TLS behaves like a local variable. A local variable dies when its function returns; a TLS value survives across invocations of the function within the same thread.
- Believing cancellation is always possible. It is not: with the state disabled,
pthread_cancelcannot cancel the thread at all; with state enabled and mode deferred, it takes effect only at cancellation points. - Handling every signal in every thread. Synchronous signals belong to the thread that caused them; broadcasting them to all threads is wrong for hardware events (the professor's option 1 is the hardware case).
- Duplicating all threads after fork when exec follows. The duplicated threads are destroyed by exec anyway — duplicate only the calling thread and save the work.
Recap + bridge. Multithreaded programs must decide for each shared resource: how fork duplicates threads (all, or only the caller, depending on whether exec follows), where signals are delivered (causing thread, all threads, or a designated thread), when cancellation may strike (state enabled, mode asynchronous or deferred-at-cancellation-points), and what data is private (thread-local storage). The lecture now answers how the kernel and the thread library coordinate all of this in the many-to-many and two-level models.
These four issues are everyday engineering decisions: web servers choose their fork-and-exec strategy when spawning helper processes, browsers cancel page-loading threads asynchronously when you press Stop, database connection pools keep per-connection state in thread-local storage, and signal-driven servers designate dedicated signal threads so that Ctrl+C handling never races with request processing.
4.12 Scheduler Activations: Lightweight Processes and Upcalls
4.12.1 The Lightweight Process (LWP)
The scheduler takes care of the activation of threads. In the many-to-many and two-level models, we need to maintain the number of kernel threads relative to the number of user threads. For this we have an intermediate data structure called the lightweight process (LWP). The LWP is like a virtual processor: it schedules which thread is going to run. In the two-level model we have many user threads and the same number of — or fewer — kernel threads, and we assign a user thread to a particular kernel thread with the help of this virtual processor. Each kernel thread has a lightweight process of its own, so we have to decide how many lightweight processes (virtual processors) to create: if there are four kernel threads, there will be four lightweight processes.
Hook. In the many-to-many and two-level models, user threads outnumber kernel threads — so the thread library must decide which user thread rides each kernel thread, and the kernel must tell the library when a ride frees up. Both sides need a middleman: the lightweight process.
The lightweight process (LWP). In the many-to-many and two-level models, the kernel threads and the user threads are two separate populations, and someone must maintain the mapping between them. That someone is the lightweight process (LWP) — an intermediate data structure that sits between the two levels. The professor's picture: the LWP is like a virtual processor. To the thread library it looks like a processor the application can schedule a user thread onto; underneath, each LWP is attached to exactly one kernel thread, and the kernel schedules the kernel threads onto the real hardware.
The count rule follows from the definition: every kernel thread carries a lightweight process of its own, so the library must decide how many virtual processors to create — if there are four kernel threads, there will be four LWPs. A process with one LWP behaves like a traditional single-threaded process; an I/O-heavy application needs about one LWP per concurrent blocking call, because each blocked kernel thread blocks its LWP with it.
4.12.2 Upcalls and the Upcall Handler
Each lightweight process provides an upcall — a communication mechanism from the kernel to the thread library — and that communication is done with the help of the upcall handler. The upcall handler runs inside the lightweight process and provides an interface between the user thread and the kernel thread. If something blocks — if a user thread gets suspended or blocked because of some other activity — the next user thread is immediately assigned, through the lightweight process, to the kernel thread. Later, when the previous user thread resumes, the LWP switches back over to it, and it communicates again with the kernel thread. This is how the activation of the scheduler takes place with the help of upcalls in the many-to-many and two-level models.
Upcalls and the upcall handler. Communication between the kernel and the thread library normally flows one way: the library calls down into the kernel (system calls). But in the many-to-many and two-level models the kernel must also inform the library about events — that upward communication is an upcall, and each LWP provides it. The upcall handler is the library's routine that processes upcalls: it runs inside the lightweight process and acts as the interface between the user thread and the kernel thread.
The activation procedure works in three acts. Act 1 — block: when the running user thread blocks (suspended on I/O or some other activity), the kernel makes an upcall to the library identifying that thread; the upcall handler saves the blocked thread's state, and the LWP immediately assigns the next ready user thread to the kernel thread. Act 2 — resume notice: when the event the blocked thread was waiting for occurs, the kernel makes another upcall; the handler marks the thread eligible to run again. Act 3 — switch back: when the LWP next has a free slot, it switches back to the resumed thread, which resumes communicating with the kernel thread. This kernel-to-library signaling is exactly how the scheduler's activation of threads takes place in the many-to-many and two-level models — the LWP is the channel, the upcall handler is the operator.
Worked trace: two kernel threads, three user threads, one block. Suppose the process has LWPs L1 and L2 (one per kernel thread) and user threads U1, U2, U3, with U1 currently running on L1 and U2 running on L2.
- U1 issues a read that blocks. The kernel makes an upcall to the library through L1's upcall handler.
- The upcall handler saves U1's state and schedules U3 onto L1 — U3 immediately begins running on kernel thread K1.
- The read completes. The kernel makes a second upcall; the handler marks U1 eligible to run again.
- When L1 becomes free, the LWP switches back to U1, which resumes its communication with kernel thread K1.
Sense-check: at every moment each LWP runs exactly one user thread on its kernel thread; a blocking user thread never idles a kernel thread, because the upcall mechanism instantly swaps in another user thread. That is the whole point of the design: no kernel thread sits idle waiting for a blocked user thread.
Common pitfalls for this concept.
- Thinking the LWP is a thread. It is a virtual processor — a scheduling interface between user threads and kernel threads — not itself a thread. The professor's "like a virtual processor" is the correct mental model.
- Believing user threads map directly to kernel threads in these models. They do not; the LWP sits between, and the number of LWPs equals the number of kernel threads, not the number of user threads.
- Assuming communication is only downward. Normal calls go from the library to the kernel (system calls); the upcall is the reverse direction — the kernel informing the library — and that is the mechanism that makes scheduler activations work.
- Forgetting which models need this machinery. Only the many-to-many and two-level models need LWPs and upcalls; in one-to-one models each user thread already has its own kernel thread, so no middleman is needed.
Recap + bridge. The lightweight process is a virtual processor — one per kernel thread — that maps user threads onto kernel threads in the many-to-many and two-level models; upcalls let the kernel tell the library that a thread blocked or unblocked, so the upcall handler can keep every kernel thread busy. That closes the session's technical arc: from why we thread, through models and APIs, to how the kernel and library cooperate. Only the exam guidance and industry applications remain.
In practice, this is the machinery behind Solaris's historical threading architecture (processes → user threads → LWPs → kernel threads) and the ancestor of modern user-level scheduling: Go's runtime, Java's virtual threads, and Windows user-mode scheduling all implement variants of the same idea — lightweight user-managed executions activated over a smaller number of kernel threads.
Exam Guidance Summary
- Quiz 1: Runs between February 12 and 18 through an announcement on the portal. Expect about 20 questions to answer within 25 to 30 minutes, with a weight of 5 percent. Quiz 1 covers everything up to the fifth contact session — that includes process scheduling, whose problems will be worked out in the next session. The scheduling problems in the quiz will differ from the ones solved in class, so review the notes to prepare.
- Quiz 2: Comes after the mid-semester exam.
- Assignment: It takes the form of a lab simulation, with a demo shown after the mid-semester exam, around the tenth contact session. It can be done individually or in a group, but a group may not have more than three members.
- Mid-semester: Covers the material up to process synchronization. Process synchronization is a very big topic that will take at least two sessions to cover.
- Amdahl's law (section 4.6): Expect a numerical: given a serial portion (often as a percentage) and a number of cores , compute the speedup with . The two-core, 25%-serial case gives 1.6. Work in four steps: convert the percentage to a decimal, compute , divide by , and take the reciprocal — and remember the limit as the ceiling.
- Threading models (section 4.8): Know which operating system uses which model, including Solaris's switch from two-level to one-to-one at version 9, and the Windows fiber package.
- Threading issues (section 4.11): Be ready for the two fork versions and when each applies, the signal delivery options, and the state/mode rules of thread cancellation.
- Threading basics (sections 4.1–4.5): The recurring exam-ready definitions are the thread-vs-process contrast (heavyweight vs lightweight), the shared/per-thread layout rule (code, data, files shared; registers and stack per-thread), and the parallelism-vs-concurrency distinction — all three are short-definition candidates.
Key Industry Applications
- Real-world: multithreaded web servers create a thread per client request instead of serving clients one at a time.
- Real-world: browsers and online shops run page loading, media playback, and cart operations in parallel threads.
- Real-world: Oracle SPARC T4 — 8 cores with 8 hardware threads per core; 4 cores give 32 threads. Chips with 16 cores are common, and NVIDIA and AMD ship up to 64 cores, so thread counts multiply with core counts.
- Real-world: POSIX pthreads on Solaris, Linux, and Mac OS; Windows threads; Java threads via the
Runnableinterface andstart(). - Real-world: implicit threading in production systems — Windows 8 thread pools and pool functions, OpenMP directives in C/C++/Fortran, and Grand Central Dispatch on Mac OS and iOS with serial and concurrent queues.
- Real-world: threading models in actual operating systems — many-to-one in Solaris green threads and GNU Portable Threads; one-to-one in Windows, Linux, and Solaris 9 and later; many-to-many in older Solaris versions and the Windows thread fiber package; two-level in IRIX, HP-UX, Tru64, and Solaris 8 and earlier.
- Real-world: Amdahl's law as the industry planning tool — companies use it to decide how many cores a workload can profitably use, since the serial fraction caps the gain at even on today's 64-core chips.
OS Lecture 4 notes · Threads and Multithreading
Sections Breakdown
Why applications use threads: the word processor and browser examples, the definition of a thread as a lightweight process, threads versus processes, and multithreaded servers.
The five benefits of multithreaded programming: responsiveness, resource sharing, economy, cheaper context switching, and scalability.
Multiprocessor and multi-core systems, the five challenges of using many cores well, and the parallelism-versus-concurrency distinction.
Data parallelism versus task parallelism, the Oracle SPARC T4 thread-count example, and concurrent versus parallel execution.
The layout of a threaded process: code, data, and files shared; registers and stack per thread.
Amdahl's law: the speedup formula, a fully worked two-core and four-core example, the 1/S limit, and student questions.
User threads and kernel threads: where each executes, who manages them, and the common thread libraries.
The four multithreading models: many-to-one, one-to-one, many-to-many, and the two-level model.
Thread libraries and APIs: pthreads, Windows threads, and Java threads with worked summation programs.
Implicit threading: thread pools, OpenMP, and Grand Central Dispatch, plus a student question on thread dependencies.
Issues in multithreaded programming: fork and exec, signal handling, thread cancellation, and thread-local storage.
Scheduler activations: the lightweight process as a virtual processor and upcalls between kernel and thread library.
The professor's exam strategy: quiz and assignment logistics, Amdahl's law numericals, threading-model mappings, and the threading issues.
Real-world uses of threads: web servers, browsers, SPARC T4 hardware threads, thread APIs, implicit threading, and threading models in operating systems.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Why Threads: Motivation and Definition
Must-know: A thread is a lightweight process: an independent flow of control owning only a program counter, registers, and a stack, while sharing the process's code, data, and files.
Top pitfall: Treating threads as visible programs or thinking a thread owns its code and data; only registers and the stack are per-thread.
Self-check: Why is creating a thread cheaper than creating a process?
Connects to: Benefits of Multithreaded Programming; Multiprocessor and Multi-Core Systems; Single-Threaded and Multithreaded Processes; Issues in Multithreaded Programming.
Benefits of Multithreaded Programming
Must-know: The five benefits of multithreading: responsiveness, resource sharing, economy, context switching, scalability; all except scalability hold on a single core.
Top pitfall: Assuming threads run truly in parallel on one core, or that resource sharing is safe without synchronization.
Self-check: Which of the five benefits requires a multiprocessor machine?
Connects to: Why Threads: Motivation and Definition; Multiprocessor and Multi-Core Systems; Amdahl's Law.
Multiprocessor and Multi-Core Systems
Must-know: Parallelism requires multiple cores and means tasks genuinely overlap in time; concurrency interleaves tasks on even one core. A single core gives concurrency only.
Top pitfall: Using concurrency and parallelism as synonyms, or assuming a multi-core machine automatically runs programs in parallel.
Self-check: Can a single-core system provide parallelism? Can a multi-core system provide concurrency?
Connects to: Benefits of Multithreaded Programming; Data Parallelism and Task Parallelism.
Data Parallelism and Task Parallelism
Must-know: Data parallelism: same operation on data subsets; task parallelism: distinct operations per thread. Max parallel threads = cores x hardware threads per core. Loop passes come from the bounds: i < 10 from i = 1 gives 9 passes (sum 45); i <= 10 gives 10 (sum 55).
Top pitfall: Counting loop passes from memory instead of the written bounds, or blurring data vs task parallelism.
Self-check: A machine has 4 cores with 8 hardware threads per core. How many threads can run in full parallel?
Connects to: Multiprocessor and Multi-Core Systems; Single-Threaded and Multithreaded Processes.
Single-Threaded and Multithreaded Processes
Must-know: Code, data, and files are shared across threads; registers and stack are per-thread. A single-threaded process has one thread using all of them.
Top pitfall: Believing threads own their code and data, or that the stack is shared because the address space is shared.
Self-check: Which components of a process are private to each thread, and which are shared?
Connects to: Why Threads: Motivation and Definition; Issues in Multithreaded Programming.
Amdahl's Law
Must-know: Speedup(N) = 1/(S + (1-S)/N) with S the serial fraction as a decimal and N the number of cores; the limit as N approaches infinity is 1/S, so the serial fraction is the bottleneck. N=2, S=0.25 gives 1.6; N=4 gives about 2.29.
Top pitfall: Substituting the percentage directly instead of the decimal (25% must be 0.25), or confusing the serial fraction S with the parallel fraction 1 - S.
Self-check: A program is 10% serial. What is the maximum speedup possible with any number of cores?
Connects to: Benefits of Multithreaded Programming; Multiprocessor and Multi-Core Systems; Data Parallelism and Task Parallelism.
User Threads and Kernel Threads
Must-know: Kernel threads are managed by the kernel and can run in parallel; user threads are managed by a user-level library, are cheap, but a single blocking system call can stall the whole process.
Top pitfall: Assuming user threads can run in parallel on multiple cores without kernel support, or that a blocking user thread only blocks itself.
Self-check: Who schedules a user thread, and who schedules a kernel thread?
Connects to: Multithreading Models; Thread Libraries and Thread APIs.
Multithreading Models
Must-know: Many-to-one cannot give parallelism; one-to-one costs one kernel thread per user thread; many-to-many needs fewer kernel threads than user threads; two-level adds binding of a user thread to a specific kernel thread. Solaris switched to one-to-one at version 9.
Top pitfall: Thinking many-to-one runs threads in parallel, or that many-to-many requires matching counts.
Self-check: Which model do Windows, Linux, and Solaris 9 and later use?
Connects to: User Threads and Kernel Threads; Scheduler Activations: Lightweight Processes and Upcalls.
Thread Libraries and Thread APIs
Must-know: All three libraries follow create -> run -> join: pthread_create/pthread_join (C/POSIX), CreateThread/WaitForSingleObject (Windows), start() invoking run() and join() (Java). pthreads is an IEEE specification, not an implementation.
Top pitfall: Calling Java run() directly instead of start() (no new thread is created), or printing the sum before joining the child thread.
Self-check: Which Java method actually creates the new thread and invokes run()?
Connects to: User Threads and Kernel Threads; Multithreading Models; Implicit Threading.
Implicit Threading
Must-know: Implicit threading delegates to compiler/runtime: thread pools reuse ready threads; OpenMP uses #pragma omp parallel for; GCD dispatches ^ blocks from serial (one at a time, FIFO) or concurrent (several in parallel, FIFO departure) queues. Shared data in memory forces dependent threads to wait — concurrency, not parallelism.
Top pitfall: Believing a concurrent GCD queue preserves completion order, or that a pool of 10 threads runs 10 threads in parallel on fewer cores.
Self-check: What distinguishes a serial GCD queue from a concurrent one?
Connects to: Data Parallelism and Task Parallelism; Single-Threaded and Multithreaded Processes; Thread Libraries and Thread APIs; Issues in Multithreaded Programming.
Issues in Multithreaded Programming
Must-know: fork after exec: duplicate only the calling thread; fork without exec: duplicate all threads. Cancellation: state must be enabled; deferred (the default) acts at cancellation points like pthread_testcancel, asynchronous cancels immediately. TLS: per-thread static data surviving across function invocations.
Top pitfall: Confusing TLS with shared static data, or believing a disabled cancellation state can still cancel a thread.
Self-check: When should fork duplicate only the thread that invoked it?
Connects to: Single-Threaded and Multithreaded Processes; Implicit Threading; Scheduler Activations: Lightweight Processes and Upcalls.
Scheduler Activations: Lightweight Processes and Upcalls
Must-know: One LWP per kernel thread; the LWP is a virtual processor for mapping user threads; upcalls are kernel-to-library communication handled by the upcall handler, which swaps in the next user thread when one blocks.
Top pitfall: Treating the LWP as a thread instead of a virtual processor, or forgetting that only many-to-many and two-level models need LWPs and upcalls.
Self-check: How many lightweight processes exist for four kernel threads?
Connects to: Multithreading Models; Issues in Multithreaded Programming.
Exam Guidance Summary
Must-know: Exam items: Amdahl's law numerical (S as percentage, N cores), which threading model each OS uses (Solaris 9 switches to one-to-one, Windows fibers), and the threading issues (two fork versions, signal delivery options, cancellation state/mode).
Top pitfall: Scheduling problems on the quiz will differ from those solved in class — reviewing only the worked problems is not enough.
Self-check: When does Quiz 1 run and what does it cover?
Connects to: Why Threads: Motivation and Definition; Amdahl's Law; Multithreading Models; Issues in Multithreaded Programming.
Key Industry Applications
Must-know: Threads appear in web servers (thread per request), browsers (parallel page/media/cart tasks), SPARC T4 (cores x hardware threads), pthreads/Windows/Java APIs, implicit threading (pools, OpenMP, GCD), and the four threading models across real operating systems.
Self-check: Which real-world systems use the many-to-one and two-level threading models?
Connects to: Why Threads: Motivation and Definition; Data Parallelism and Task Parallelism; Multithreading Models; Thread Libraries and Thread APIs; Implicit Threading.
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.