25-Comp-B10 Distributed Systems · May 2015
Nivaar worked solution (AI-drafted; not reviewed by a licensed engineer)
Question text not reproduced: the examination questions are © Engineers and Geoscientists BC. Open the official past paper (linked at the top of this page) to read the question, then follow the worked solution below.
Given. Cache hit rate 80% (miss rate 20%); on a hit, a request costs 6 ms of CPU time; on a miss, an additional 10 ms of disk I/O is required on top of the same 6 ms CPU cost; three server configurations to evaluate: single-threaded, two threads on one CPU, and two threads on two CPUs.
| Quantity | Value |
|---|---|
| Cache hit probability | 0.80 |
| Cache miss probability | 0.20 |
| CPU time per request (hit or miss) | 6 ms |
| Extra disk I/O time on a miss | 10 ms |
Find. The average throughput (requests/second) the server sustains under each of the three threading/processor configurations.
Approach. Model the server as demanding two separable resources per request — CPU time (always 6 ms) and disk time (10 ms, but only on the 20% of requests that miss) — then find each configuration's throughput as the reciprocal of whichever resource-and-concurrency combination limits it: total serial time per request when nothing can overlap, or the busier of the two resources once concurrency lets disk waits overlap with other requests' CPU work.
| Configuration | Bottleneck | Throughput |
|---|---|---|
| 1. Single-threaded | total serial time (no overlap) | 125.00 req/s |
| 2. Two threads, one CPU | CPU (6 ms/request > 2 ms avg. disk) | 166.67 req/s |
| 3. Two threads, two CPUs | CPU (333.33 req/s < disk's 500 req/s) | 333.33 req/s |
(b) Thread-per-request vs. worker-pool architecture. In a thread-per-request server, a fresh thread is created for every incoming request and destroyed when that request completes. This gives the simplest programming model (each thread's code reads like a straight-line sequential handler with no shared scheduling state to reason about) and naturally scales concurrency to the offered load, but thread creation/destruction is not free — under a heavy or bursty request rate, the server pays that overhead on every single request and can exhaust kernel resources (thread-table entries, stack memory) if too many requests arrive at once, since there is no built-in cap on how many threads can exist simultaneously. In a worker-pool architecture, a fixed (or bounded, elastically-sized) pool of threads is created once at startup; each incoming request is placed on a shared queue, and idle worker threads pull the next request off that queue and process it, returning to the pool afterward instead of terminating. This amortizes thread-creation cost across the server's whole lifetime and bounds resource consumption predictably (the pool size caps how much concurrent work the server will ever attempt), at the cost of a request occasionally waiting in the queue if all workers are currently busy, and of slightly more complex code (a shared, thread-safe work queue) than the thread-per-request model needs. In practice, high-throughput production servers favour the worker-pool model precisely because of its predictable resource bound under load, reserving thread-per-request for simpler or lower-volume services where its programming simplicity outweighs the creation overhead.
(c) Kernel support for user-level threads. A pure user-level threading implementation (a run-time library, such as an early JVM's "green threads" on UNIX) multiplexes many application-level threads onto one kernel-visible process/task, doing its own scheduling, context-switching and stack management entirely in user space, without the kernel ever being aware that more than one logical thread exists. For this to work at all, the kernel must still provide: (1) some mechanism the library can use to yield control back to itself periodically without kernel help — typically the library relies on cooperative yielding at library call boundaries, or on a periodic timer/alarm signal the kernel delivers to the process, which the library's signal handler intercepts to force a preemptive context switch between user threads; (2) non-blocking (or at least library-interceptable) I/O primitives — the library must be able to substitute its own wrapper around a blocking system call (e.g. read) so that when one user thread would block, the library can switch to another ready user thread instead of the whole kernel-level task blocking; and (3) enough raw process/task abstraction (address space, a stack region the library can carve up and manage itself) for the library to build its own thread control blocks on top of.
Yes, page faults are a genuine problem for user-level threads. Because the kernel schedules the whole process (not the individual user-level threads inside it) as a single unit, a page fault taken by one user thread blocks in the kernel exactly as an ordinary blocking system call would — and since the kernel has no visibility into the library's other, perfectly runnable user threads, the entire process, and therefore every user-level thread within it, is suspended until the fault is serviced, even though most of those threads had nothing to do with the faulting memory access. This defeats one of the main reasons for using threads (letting other work proceed while one thread waits) and is a core reason production systems moved toward kernel-level threads (or a hybrid many-to-many model) rather than pure user-level threading, since a kernel-level scheduler can simply run a different kernel thread while one blocks on a page fault.