COMP 3150Operating Systems

Unit 4 · Self-check

Unit 4 Self-Check Quiz

Twenty-four questions covering the thread abstraction, threading models, multicore concurrency, POSIX threads, and thread-management strategies. Each question can be expanded to reveal a model answer.

24 questions Self-paced Ungraded

Questions

Multiple choice and short answer

Answer each question before expanding. These span all five lessons of Unit 4.

Question 1

What exactly does a thread share with other threads of the same process, and what does it own privately?

Show answer
Threads of the same process share the address space (text, data, heap), the open file descriptor table, the current working directory, signal dispositions, user and group credentials, and other process-level resources. Each thread owns privately its program counter, register set, stack (including its local variables and call frames), thread ID, signal mask, errno, and any thread-local storage. The split is what makes threads lightweight: creating a thread does not require a new address space or new kernel data structures for file tables.

Question 2

Which of the following is not a benefit of multithreading over multiple processes?

Show answer
Isolation. Threads in the same process share the address space, so a wild pointer in one thread can corrupt another thread's data, and a fatal signal (such as SIGSEGV) terminates the entire process, not just the offending thread. Isolation is a benefit of processes, not threads, and is one of the reasons some systems (for example, Chrome's site-per-process model) choose processes over threads for fault containment.

Question 3

In the many-to-one threading model, what happens when one user-level thread blocks on a system call?

Show answer
All threads block. The kernel knows only one schedulable entity — the process — so it cannot distinguish the blocked thread from the others. The user-level library may have other runnable threads, but it cannot run them because the kernel has put the only kernel thread to sleep. This is the central weakness of the many-to-one model. It is why the model is unsuitable for programs that make blocking system calls and why it fell out of favour after the rise of multicore hardware, despite its low overhead.

Question 4

Which threading model maps each user thread to exactly one kernel thread?

Show answer
One-to-one. Each user thread is backed by a distinct kernel thread, so if one blocks, the others continue, and the kernel can schedule them independently across cores. This is the model used by Linux's NPTL and by Windows. Its cost is that creating a user thread now requires a kernel thread, so thread creation is more expensive than in the many-to-one model, and a program that creates thousands of threads will burden the kernel's scheduler.

Question 5

What is the essential idea of the many-to-many threading model, and what problem does it solve?

Show answer
The model multiplexes many user-level threads onto a smaller or equal number of kernel threads. A user-level scheduler decides which user threads run on the available kernel threads, and the kernel schedules the kernel threads. This solves two problems at once: it allows a program to create very many user threads cheaply (the kernel is not burdened with one kernel thread per user thread), and it still allows true concurrency because several kernel threads are available to the kernel scheduler. It also avoids the "one blocking call blocks all" problem of many-to-one, because a blocked user thread on one kernel thread does not stop other kernel threads from running.

Question 6

Which of the following best distinguishes task parallelism from data parallelism?

Show answer
The first choice. Task parallelism assigns different operations to different threads (for example, one thread computes the FFT, another renders the spectrum, a third handles I/O). Data parallelism assigns different portions of the same data to different threads (for example, splitting a large array among n threads, each applying the same operation to its slice). Real programs usually combine both.

Question 7

State Amdahl's Law and explain its implication for multicore scaling.

Show answer
Amdahl's Law says that if a fraction p of a computation can be parallelised and the remaining fraction (1 − p) is strictly serial, the maximum speedup achievable with n processors is:
S(n) = 1 / ((1 − p) + p / n)
As n → ∞, the speedup approaches 1 / (1 − p). If 20% of the work is serial, the maximum speedup is fivefold regardless of how many cores are added. The implication is that adding cores to a program with a significant serial fraction yields diminishing returns; serious parallel programs must attack the serial portion (the critical section, the sequential I/O, the startup costs) as well as the parallel portion.

Question 8

Why must a multithreaded program's critical sections be designed with care even when the program is correct on a single core?

Show answer
On a single core, threads run one at a time and the hardware interleaves them only at well-defined points (system calls, timer ticks, explicit yields). On multiple cores, threads truly execute simultaneously, and operations that appeared atomic — an i++, a pointer update, a check-then-act — are no longer atomic. Two threads can read the same location, both compute the new value, and both write back, losing one update. Additionally, the memory consistency model of the hardware permits stores to be reordered as seen by other cores unless fences or atomic operations are used. Correctness on one core therefore does not imply correctness on multiple cores.

Question 9

Which of the following is the correct signature of pthread_create?

(a) int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                       void *(*start_routine)(void *), void *arg);

(b) void pthread_create(pthread_t thread, void (*start)(void));

(c) int pthread_create(pthread_t *thread, void *start_routine(void *), void *arg);

(d) pthread_t pthread_create(void *(*start)(void *), int attr);
Show answer
The correct answer is (a). The four arguments are: a pointer to a pthread_t that receives the new thread's identifier; a pointer to a thread-attribute object (or NULL for defaults); a function pointer of type void *(*)(void *) — the entry point; and a single void * argument that is passed through to the entry point. The function returns 0 on success and a non-zero error code on failure; it does not set errno. The single void * argument is the standard mechanism for passing multiple values: pack them into a struct and pass a pointer to it.

Question 10

What is the difference between pthread_exit and returning from the thread's start routine?

Show answer
Returning from the start routine is equivalent to calling pthread_exit with the return value as its argument, provided the return value is not a pointer to a local variable. The pointer is stored in the thread's exit status and can be retrieved by pthread_join. The distinction matters when a thread must exit from a nested function: returning from the start routine is impossible from a deeper call, so pthread_exit is the only way to terminate the thread with a status. The other distinguishing feature is that returning from main terminates the entire process, whereas pthread_exit from main terminates only the calling thread — the other threads continue. That subtlety has caused many debugging sessions.

Question 11

Which of the following is a correct statement about pthread_join?

Show answer
The first choice. pthread_join can be called from any thread — not just the main thread — and it blocks until the target thread terminates. After it returns, the target thread's resources are released and a second pthread_join for the same thread is undefined behaviour (in practice, it commonly fails with ESRCH or crashes). If the thread is detached (via pthread_detach or an attribute), pthread_join fails with EINVAL, because a detached thread's resources are released automatically on exit.

Question 12

What are the two cancellation modes in POSIX threads, and how do they differ in their effect on the target thread?

Show answer
Deferred cancellation (PTHREAD_CANCEL_DEFERRED) is the default: a cancellation request is recorded, but the target thread is not terminated until it reaches a cancellation point — a function such as read, write, sleep, pthread_cond_wait, or an explicit pthread_testcancel. This gives the thread a chance to release resources in a cancellation handler. Asynchronous cancellation (PTHREAD_CANCEL_ASYNCHRONOUS) terminates the thread immediately, at any instruction. It is dangerous because the thread may hold a mutex or have allocated memory; the program can deadlock or leak. Deferred cancellation is almost always the right choice.

Question 13

Why is fork problematic in a multithreaded program, and what does POSIX say about it?

Show answer
In a multithreaded program, only the calling thread survives the fork in the child — the other threads are not duplicated. But the child inherits the entire address space, including all the mutexes, semaphores, and condition variables of the parent. If a mutex was held by a thread that did not survive, the child has a locked mutex with no one to unlock it, and any attempt to acquire it in the child deadlocks. POSIX addresses this by specifying only a small set of async-signal-safe functions that may be called between fork and exec in a multithreaded program; anything else is undefined. The clean pattern is: fork, then immediately exec, without touching shared state in between. Alternatively, use pthread_atfork to register prepare/parent/child handlers that restore consistency.

Question 14

What is a thread pool, and what problem does it solve?

Show answer
A thread pool is a fixed set of worker threads created at startup, plus a queue of tasks. When a request arrives, it is placed on the queue; an idle worker dequeues it and executes it. The pool solves three problems: it bounds the number of threads (so the kernel scheduler is not overwhelmed by a burst of thousands of short-lived threads); it amortises thread creation cost (threads are created once, not per task); and it decouples task submission from task execution, which simplifies the program structure. Pools are the standard pattern for web servers, database engines, and any server that must handle high request rates.

Question 15

Which of the following is not an appropriate use of thread-local storage (TLS)?

Show answer
Storing a global configuration value shared by all threads. That should be a normal global (or a read-only value in the text segment), because by definition it is shared. TLS is for values that are conceptually per-thread: errno, PRNG state, scratch buffers, user identity in a server worker, and so on. Using TLS for shared state hides the sharing and creates consistency problems — different threads may see stale values.

Question 16

What is a reentrant function, and why is reentrancy important for thread safety?

Show answer
A reentrant function is one that can be safely called by multiple threads (or by the same thread at multiple points of control) without causing incorrect results. It does not use global or static mutable state, does not return a pointer to a shared buffer, and does not call other non-reentrant functions. Reentrancy is a strong form of thread safety: a reentrant function needs no locks because it has no shared state to protect. Many library functions have reentrant variants — strtok_r for strtok, localtime_r for localtime, readdir_r for readdir — precisely because the originals use static buffers.

Question 17

What is the difference between thread-safe and asynchronous-signal-safe?

Show answer
Thread-safe means the function can be called concurrently by multiple threads without incorrect behaviour; it typically achieves this with internal locks. Asynchronous-signal-safe is a stronger property: the function can be called from within a signal handler without risk of deadlock or corruption. A signal handler can interrupt a thread at any point — possibly while it holds a lock inside a thread-safe function — so calling that function from the handler would deadlock. POSIX defines a short list of async-signal-safe functions (write, read, _exit, signal masking functions, and a few others). Most library functions, including printf and malloc, are not on the list.

Question 18

Which thread-synchronisation primitive is the correct tool for making a thread wait until a shared predicate becomes true?

Show answer
The third choice. A mutex protects the shared data; a condition variable lets a thread block until the predicate changes, and it re-checks the predicate on wakeup. This combination is the canonical pattern for "wait until something is true". A mutex alone cannot express waiting (it can only protect a critical section). A semaphore can count resources but cannot express an arbitrary predicate. A barrier is for phase synchronisation among a fixed set of threads, not for predicate-based waiting.

Question 19

In the fork/join model, what is the role of the join, and how does it differ from a barrier?

Show answer
In fork/join, a parent task forks several child tasks, then joins them — it blocks until each child has finished — and combines their results. The join is one-directional: the parent waits for the children, but the children do not wait for each other. A barrier, by contrast, is symmetric: every participating thread blocks until all have arrived, and then all proceed together. Barriers are used for iterative algorithms where each round depends on the previous round; fork/join is used for divide-and-conquer where subproblems are independent.

Question 20

A program creates 16 threads on a machine with 4 cores. Which of the following is true?

Show answer
At most four threads are executing simultaneously at any instant; the remaining twelve are runnable and waiting for a core. The kernel's scheduler multiplexes them across the four cores, giving each a time slice. The total throughput depends on how much of each thread's work is independent: if the threads contend for a single shared lock, the effective concurrency may be near one, and the program may run slower than a single-threaded version because of the switching and lock overhead. If the threads are independent and CPU-bound, throughput should be close to four times the single-threaded rate, minus the serial fraction (Amdahl's Law).

Question 21

What is the difference between a mutex and a spinlock, and when is a spinlock appropriate?

Show answer
A mutex blocks a waiting thread — the kernel deschedules it and wakes it when the lock is released. A spinlock makes the waiting thread loop, repeatedly testing the lock and burning CPU cycles until it succeeds. A spinlock is appropriate only when the expected wait is shorter than the cost of two context switches (roughly a few hundred nanoseconds to a microsecond). This is common inside kernel code that protects very short critical sections and that cannot afford to sleep. In user space, spinlocks are rarely justified; a mutex or an adaptive lock (which spins briefly, then sleeps) is almost always better.

Question 22

Why is it important to check the return value of pthread_create, and what does a non-zero return mean?

Show answer
pthread_create returns 0 on success and a non-zero error code on failure — it does not set errno, so you cannot rely on perror or strerror(errno). A non-zero return means the thread was not created; the reasons include EAGAIN (insufficient resources — too many threads or not enough memory), EINVAL (invalid attributes), and EPERM (no permission to set the requested scheduling policy). Ignoring the return value is a common bug: the program continues as if the thread existed, and pthread_join on the uninitialised pthread_t produces undefined behaviour.

Question 23

Explain why a thread's stack is normally small (a few megabytes) and what happens if it overflows.

Show answer
Each thread has its own stack, and threads are meant to be numerous, so stacks cannot each be as large as a process's main stack. The default on Linux is typically 8 MB for the main thread's stack, but new threads commonly get 1–2 MB (configurable via pthread_attr_setstacksize). If a thread recurses too deeply or allocates a large local array, the stack overflows. On Linux, a guard page at the end of the stack triggers a SIGSEGV; the default action terminates the entire process. There is no "stack grows into heap" safety net for threads. Deep recursion or large local data should be moved to the heap or the stack size increased.

Question 24

In one or two sentences, state the key insight that makes threads the right abstraction for most concurrent server programs.

Show answer
Threads let a program express concurrency in the natural unit of the problem — one thread per request, one thread per connection, one thread per pipeline stage — while sharing the address space so that no data must be copied or serialised to pass between them. The result is code that is easier to write than an event loop, and cheaper to run than a process-per-request design, because the kernel does not have to create a new address space for each unit of concurrency.

Ready for the graded quiz?

Once you are comfortable with every question above, take the graded Unit 4 quiz in the LMS. It covers the same material with a mix of multiple-choice, short-answer, and short programming problems.

👤